forked from Boria138/PortProtonQt
Compare commits
8 Commits
eb90836710
...
v0.1.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
8e11dac987
|
|||
|
358afbdbdb
|
|||
|
83730499e2
|
|||
|
84f560ed30
|
|||
|
888c9ac387
|
|||
|
68d06ca05c
|
|||
|
6923a5f05c
|
|||
|
f3f85441d8
|
@@ -8,7 +8,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
# Common version, will be used for tagging the release
|
# Common version, will be used for tagging the release
|
||||||
VERSION: 0.1.4
|
VERSION: 0.1.5
|
||||||
PKGDEST: "/tmp/portprotonqt"
|
PKGDEST: "/tmp/portprotonqt"
|
||||||
PACKAGE: "portprotonqt"
|
PACKAGE: "portprotonqt"
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
Все заметные изменения в этом проекте фиксируются в этом файле.
|
Все заметные изменения в этом проекте фиксируются в этом файле.
|
||||||
Формат основан на [Keep a Changelog](https://keepachangelog.com/) и придерживается принципов [Semantic Versioning](https://semver.org/).
|
Формат основан на [Keep a Changelog](https://keepachangelog.com/) и придерживается принципов [Semantic Versioning](https://semver.org/).
|
||||||
|
|
||||||
## [Unreleased]
|
## [0.1.5] - 2025-08-31
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Больше типов анимаций при открытии карточки игры (подробности см. в документации).
|
- Больше типов анимаций при открытии карточки игры (подробности см. в документации).
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
- Пункт "Выход" в трей.
|
- Пункт "Выход" в трей.
|
||||||
- Пункт "Темы" в трей для быстрого переключения тем.
|
- Пункт "Темы" в трей для быстрого переключения тем.
|
||||||
- Двойной клик по иконке трея для показа/скрытия главного окна.
|
- Двойной клик по иконке трея для показа/скрытия главного окна.
|
||||||
|
- Запуск через трей показывает модальное окно для слежки за процессом запуска
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Уменьшена длительность анимации открытия карточки с 800 до 350 мс.
|
- Уменьшена длительность анимации открытия карточки с 800 до 350 мс.
|
||||||
@@ -29,6 +30,7 @@
|
|||||||
- Обновлены все зависимости (затрагивает только AppImage).
|
- Обновлены все зависимости (затрагивает только AppImage).
|
||||||
- Приложение теперь не закрывается полностью, а сворачивается в трей.
|
- Приложение теперь не закрывается полностью, а сворачивается в трей.
|
||||||
- Карточки теперь все находятся друг под другом, а не в разнабой
|
- Карточки теперь все находятся друг под другом, а не в разнабой
|
||||||
|
- Изменено соотношение сторон карточек
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- `legendary list` теперь не вызывается, если вход в EGS не был выполнен.
|
- `legendary list` теперь не вызывается, если вход в EGS не был выполнен.
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ AppDir:
|
|||||||
id: ru.linux_gaming.PortProtonQt
|
id: ru.linux_gaming.PortProtonQt
|
||||||
name: PortProtonQt
|
name: PortProtonQt
|
||||||
icon: ru.linux_gaming.PortProtonQt
|
icon: ru.linux_gaming.PortProtonQt
|
||||||
version: 0.1.4
|
version: 0.1.5
|
||||||
exec: usr/bin/python3
|
exec: usr/bin/python3
|
||||||
exec_args: "-m portprotonqt.app $@"
|
exec_args: "-m portprotonqt.app $@"
|
||||||
apt:
|
apt:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
pkgname=portprotonqt
|
pkgname=portprotonqt
|
||||||
pkgver=0.1.4
|
pkgver=0.1.5
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Modern GUI for managing and launching games from PortProton, Steam, and Epic Games Store"
|
pkgdesc="Modern GUI for managing and launching games from PortProton, Steam, and Epic Games Store"
|
||||||
arch=('any')
|
arch=('any')
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
%global pypi_name portprotonqt
|
%global pypi_name portprotonqt
|
||||||
%global pypi_version 0.1.4
|
%global pypi_version 0.1.5
|
||||||
%global oname PortProtonQt
|
%global oname PortProtonQt
|
||||||
%global _python_no_extras_requires 1
|
%global _python_no_extras_requires 1
|
||||||
|
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ Current translation status:
|
|||||||
|
|
||||||
| Locale | Progress | Translated |
|
| Locale | Progress | Translated |
|
||||||
| :----- | -------: | ---------: |
|
| :----- | -------: | ---------: |
|
||||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 202 |
|
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 203 |
|
||||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 202 |
|
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 203 |
|
||||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 202 of 202 |
|
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 203 of 203 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -21,9 +21,9 @@
|
|||||||
|
|
||||||
| Локаль | Прогресс | Переведено |
|
| Локаль | Прогресс | Переведено |
|
||||||
| :----- | -------: | ---------: |
|
| :----- | -------: | ---------: |
|
||||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 202 |
|
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 203 |
|
||||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 202 |
|
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 203 |
|
||||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 202 из 202 |
|
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 203 из 203 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -108,7 +108,6 @@ GAME_CARD_ANIMATION = {
|
|||||||
|
|
||||||
# Type of card animation on hover or focus
|
# Type of card animation on hover or focus
|
||||||
# Possible values: "gradient", "scale"
|
# Possible values: "gradient", "scale"
|
||||||
# "scale" is unstable and requires adjustments (use at your own risk)
|
|
||||||
# "gradient" enables a rotating gradient for the border, "scale" enlarges the card
|
# "gradient" enables a rotating gradient for the border, "scale" enlarges the card
|
||||||
"card_animation_type": "gradient",
|
"card_animation_type": "gradient",
|
||||||
|
|
||||||
|
|||||||
@@ -108,7 +108,6 @@ GAME_CARD_ANIMATION = {
|
|||||||
|
|
||||||
# Тип анимации для карточки при наведении или фокусе
|
# Тип анимации для карточки при наведении или фокусе
|
||||||
# Возможные значения: "gradient", "scale"
|
# Возможные значения: "gradient", "scale"
|
||||||
# scale крайне нестабилен и требует доработки (используйте на свой страх и риск)
|
|
||||||
# "gradient" включает вращающийся градиент для обводки, "scale" увеличивает размер карточки
|
# "gradient" включает вращающийся градиент для обводки, "scale" увеличивает размер карточки
|
||||||
"card_animation_type": "gradient",
|
"card_animation_type": "gradient",
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ logger = get_logger(__name__)
|
|||||||
|
|
||||||
__app_id__ = "ru.linux_gaming.PortProtonQt"
|
__app_id__ = "ru.linux_gaming.PortProtonQt"
|
||||||
__app_name__ = "PortProtonQt"
|
__app_name__ = "PortProtonQt"
|
||||||
__app_version__ = "0.1.4"
|
__app_version__ = "0.1.5"
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ from PySide6.QtGui import QFont, QFontMetrics, QPainter
|
|||||||
|
|
||||||
def compute_layout(nat_sizes, rect_width, spacing, max_scale):
|
def compute_layout(nat_sizes, rect_width, spacing, max_scale):
|
||||||
"""
|
"""
|
||||||
Вычисляет расположение элементов с учетом отступов и максимального масштабирования карточек.
|
Вычисляет расположение элементов с учетом отступов и возможного увеличения карточек.
|
||||||
nat_sizes: массив (N, 2) с натуральными размерами элементов (ширина, высота).
|
nat_sizes: массив (N, 2) с натуральными размерами элементов (ширина, высота).
|
||||||
rect_width: доступная ширина контейнера.
|
rect_width: доступная ширина контейнера.
|
||||||
spacing: отступ между элементами.
|
spacing: отступ между элементами (горизонтальный и вертикальный).
|
||||||
max_scale: максимальный коэффициент масштабирования (например, 1.2).
|
max_scale: максимальный коэффициент масштабирования (например, 1.0).
|
||||||
|
|
||||||
Возвращает:
|
Возвращает:
|
||||||
result: массив (N, 4), где каждая строка содержит [x, y, new_width, new_height].
|
result: массив (N, 4), где каждая строка содержит [x, y, new_width, new_height].
|
||||||
@@ -19,55 +19,89 @@ def compute_layout(nat_sizes, rect_width, spacing, max_scale):
|
|||||||
result = np.zeros((N, 4), dtype=np.int32)
|
result = np.zeros((N, 4), dtype=np.int32)
|
||||||
y = 0
|
y = 0
|
||||||
i = 0
|
i = 0
|
||||||
|
min_margin = 20 # Минимальный отступ по краям
|
||||||
|
|
||||||
|
# Определяем максимальное количество элементов в ряду и общий масштаб
|
||||||
|
max_items_per_row = 0
|
||||||
|
global_scale = 1.0
|
||||||
|
max_row_x_start = min_margin # Начальная позиция x самого длинного ряда
|
||||||
|
temp_i = 0
|
||||||
|
|
||||||
|
# Первый проход: находим максимальное количество элементов в ряду
|
||||||
|
while temp_i < N:
|
||||||
|
sum_width = 0
|
||||||
|
count = 0
|
||||||
|
temp_j = temp_i
|
||||||
|
while temp_j < N:
|
||||||
|
w = nat_sizes[temp_j, 0]
|
||||||
|
if count > 0 and (sum_width + spacing + w) > rect_width - 2 * min_margin:
|
||||||
|
break
|
||||||
|
sum_width += w
|
||||||
|
count += 1
|
||||||
|
temp_j += 1
|
||||||
|
|
||||||
|
if count > max_items_per_row:
|
||||||
|
max_items_per_row = count
|
||||||
|
# Вычисляем масштаб для самого заполненного ряда
|
||||||
|
available_width = rect_width - spacing * (count - 1) - 2 * min_margin
|
||||||
|
desired_scale = available_width / sum_width if sum_width > 0 else 1.0
|
||||||
|
global_scale = desired_scale if desired_scale < max_scale else max_scale
|
||||||
|
# Сохраняем начальную позицию x для самого длинного ряда
|
||||||
|
scaled_row_width = int(sum_width * global_scale) + spacing * (count - 1)
|
||||||
|
max_row_x_start = max(min_margin, (rect_width - scaled_row_width) // 2)
|
||||||
|
temp_i = temp_j
|
||||||
|
|
||||||
|
# Второй проход: размещаем элементы
|
||||||
while i < N:
|
while i < N:
|
||||||
sum_width = 0
|
sum_width = 0
|
||||||
row_max_height = 0
|
row_max_height = 0
|
||||||
count = 0
|
count = 0
|
||||||
j = i
|
j = i
|
||||||
|
|
||||||
# Подбираем количество элементов для текущего ряда с учетом max_scale
|
# Подбираем количество элементов для текущего ряда
|
||||||
scaled_sizes = nat_sizes * max_scale
|
|
||||||
while j < N:
|
while j < N:
|
||||||
w = scaled_sizes[j, 0]
|
w = nat_sizes[j, 0]
|
||||||
if count > 0 and (sum_width + spacing + w) > rect_width:
|
if count > 0 and (sum_width + spacing + w) > rect_width - 2 * min_margin:
|
||||||
break
|
break
|
||||||
sum_width += w
|
sum_width += w
|
||||||
count += 1
|
count += 1
|
||||||
h = scaled_sizes[j, 1]
|
h = nat_sizes[j, 1]
|
||||||
if h > row_max_height:
|
if h > row_max_height:
|
||||||
row_max_height = h
|
row_max_height = h
|
||||||
j += 1
|
j += 1
|
||||||
|
|
||||||
# Вычисляем общую ширину ряда включая отступы
|
# Используем глобальный масштаб для всех рядов
|
||||||
total_row_width = sum_width + spacing * (count - 1)
|
scale = global_scale
|
||||||
|
scaled_row_width = int(sum_width * scale) + spacing * (count - 1)
|
||||||
|
|
||||||
# Вычисляем смещение для центрирования ряда
|
# Определяем начальную координату x
|
||||||
x_offset = (rect_width - total_row_width) // 2
|
if count == max_items_per_row:
|
||||||
|
# Центрируем полный ряд
|
||||||
|
x = max(min_margin, (rect_width - scaled_row_width) // 2)
|
||||||
|
else:
|
||||||
|
# Выравниваем неполный ряд по левому краю, совмещая с началом самого длинного ряда
|
||||||
|
x = max_row_x_start
|
||||||
|
|
||||||
# Размещаем элементы в ряду с центрированием
|
|
||||||
x = x_offset
|
|
||||||
for k in range(i, j):
|
for k in range(i, j):
|
||||||
new_w = int(nat_sizes[k, 0] * max_scale)
|
new_w = int(nat_sizes[k, 0] * scale)
|
||||||
new_h = int(nat_sizes[k, 1] * max_scale)
|
new_h = int(nat_sizes[k, 1] * scale)
|
||||||
result[k, 0] = x
|
result[k, 0] = x
|
||||||
result[k, 1] = y
|
result[k, 1] = y
|
||||||
result[k, 2] = new_w
|
result[k, 2] = new_w
|
||||||
result[k, 3] = new_h
|
result[k, 3] = new_h
|
||||||
x += new_w + spacing
|
x += new_w + spacing
|
||||||
|
|
||||||
y += int(row_max_height) + spacing
|
y += int(row_max_height * scale) + spacing
|
||||||
i = j
|
i = j
|
||||||
|
|
||||||
return result, y
|
return result, y
|
||||||
|
|
||||||
class FlowLayout(QLayout):
|
class FlowLayout(QLayout):
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.itemList = []
|
self.itemList = []
|
||||||
self.setContentsMargins(0, 0, 0, 0)
|
self.setContentsMargins(20, 20, 20, 20) # Отступы по краям
|
||||||
self._spacing = 3
|
self._spacing = 20 # Отступ для анимации и предотвращения перекрытий
|
||||||
self._max_scale = 1.2
|
self._max_scale = 1.0 # Отключено масштабирование в layout
|
||||||
|
|
||||||
def addItem(self, item: QLayoutItem) -> None:
|
def addItem(self, item: QLayoutItem) -> None:
|
||||||
self.itemList.append(item)
|
self.itemList.append(item)
|
||||||
@@ -104,12 +138,10 @@ class FlowLayout(QLayout):
|
|||||||
def minimumSize(self):
|
def minimumSize(self):
|
||||||
size = QSize()
|
size = QSize()
|
||||||
for item in self.itemList:
|
for item in self.itemList:
|
||||||
# Учитываем максимальный масштаб при расчете минимального размера
|
size = size.expandedTo(item.minimumSize())
|
||||||
item_size = item.sizeHint()
|
|
||||||
scaled_size = QSize(int(item_size.width() * self._max_scale), int(item_size.height() * self._max_scale))
|
|
||||||
size = size.expandedTo(scaled_size)
|
|
||||||
margins = self.contentsMargins()
|
margins = self.contentsMargins()
|
||||||
size += QSize(margins.left() + margins.right(), margins.top() + margins.bottom())
|
size += QSize(margins.left() + margins.right(),
|
||||||
|
margins.top() + margins.bottom())
|
||||||
return size
|
return size
|
||||||
|
|
||||||
def doLayout(self, rect, testOnly):
|
def doLayout(self, rect, testOnly):
|
||||||
@@ -157,7 +189,7 @@ class ClickableLabel(QLabel):
|
|||||||
self._icon_size = icon_size
|
self._icon_size = icon_size
|
||||||
self._icon_space = icon_space
|
self._icon_space = icon_space
|
||||||
self._font_scale_factor = font_scale_factor
|
self._font_scale_factor = font_scale_factor
|
||||||
self._card_width = 250 # Значение по умолчанию
|
self._card_width = 250
|
||||||
if change_cursor:
|
if change_cursor:
|
||||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
self.updateFontSize()
|
self.updateFontSize()
|
||||||
@@ -175,28 +207,23 @@ class ClickableLabel(QLabel):
|
|||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def setCardWidth(self, card_width: int):
|
def setCardWidth(self, card_width: int):
|
||||||
"""Обновляет ширину карточки и пересчитывает размер шрифта."""
|
|
||||||
self._card_width = card_width
|
self._card_width = card_width
|
||||||
self.updateFontSize()
|
self.updateFontSize()
|
||||||
|
|
||||||
def updateFontSize(self):
|
def updateFontSize(self):
|
||||||
"""Обновляет размер шрифта на основе card_width и font_scale_factor."""
|
|
||||||
font = self.font()
|
font = self.font()
|
||||||
font_size = int(self._card_width * self._font_scale_factor)
|
font_size = int(self._card_width * self._font_scale_factor)
|
||||||
font.setPointSize(max(8, font_size)) # Минимальный размер шрифта 8
|
font.setPointSize(max(8, font_size))
|
||||||
self.setFont(font)
|
self.setFont(font)
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def paintEvent(self, event):
|
def paintEvent(self, event):
|
||||||
painter = QPainter(self)
|
painter = QPainter(self)
|
||||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||||
|
|
||||||
rect = self.contentsRect()
|
rect = self.contentsRect()
|
||||||
alignment = self.alignment()
|
alignment = self.alignment()
|
||||||
|
|
||||||
icon_size = self._icon_size
|
icon_size = self._icon_size
|
||||||
spacing = self._icon_space
|
spacing = self._icon_space
|
||||||
|
|
||||||
text = self.text()
|
text = self.text()
|
||||||
|
|
||||||
if self._icon:
|
if self._icon:
|
||||||
@@ -205,17 +232,11 @@ class ClickableLabel(QLabel):
|
|||||||
pixmap = None
|
pixmap = None
|
||||||
|
|
||||||
fm = QFontMetrics(self.font())
|
fm = QFontMetrics(self.font())
|
||||||
|
|
||||||
# Считаем, сколько места остаётся под текст
|
|
||||||
available_width = rect.width()
|
available_width = rect.width()
|
||||||
if pixmap:
|
if pixmap:
|
||||||
available_width -= (icon_size + spacing)
|
available_width -= (icon_size + spacing)
|
||||||
# Отступы по 2px с каждой стороны
|
|
||||||
available_width = max(0, available_width - 4)
|
available_width = max(0, available_width - 4)
|
||||||
|
|
||||||
# Получаем «обрезанный» текст с многоточием
|
|
||||||
display_text = fm.elidedText(text, Qt.TextElideMode.ElideRight, available_width)
|
display_text = fm.elidedText(text, Qt.TextElideMode.ElideRight, available_width)
|
||||||
|
|
||||||
text_width = fm.horizontalAdvance(display_text)
|
text_width = fm.horizontalAdvance(display_text)
|
||||||
text_height = fm.height()
|
text_height = fm.height()
|
||||||
total_width = text_width + (icon_size + spacing if pixmap else 0)
|
total_width = text_width + (icon_size + spacing if pixmap else 0)
|
||||||
@@ -285,8 +306,6 @@ class AutoSizeButton(QPushButton):
|
|||||||
|
|
||||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
self.setFlat(True)
|
self.setFlat(True)
|
||||||
|
|
||||||
# Изначально выставляем минимальную ширину
|
|
||||||
self.setMinimumWidth(50)
|
self.setMinimumWidth(50)
|
||||||
self.adjustFontSize()
|
self.adjustFontSize()
|
||||||
|
|
||||||
@@ -317,7 +336,6 @@ class AutoSizeButton(QPushButton):
|
|||||||
if not self._update_size:
|
if not self._update_size:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Определяем доступную ширину внутри кнопки
|
|
||||||
available_width = self.width()
|
available_width = self.width()
|
||||||
if self._icon:
|
if self._icon:
|
||||||
available_width -= self._icon_size
|
available_width -= self._icon_size
|
||||||
@@ -328,7 +346,6 @@ class AutoSizeButton(QPushButton):
|
|||||||
font = QFont(self._original_font)
|
font = QFont(self._original_font)
|
||||||
text = self._original_text
|
text = self._original_text
|
||||||
|
|
||||||
# Подбираем максимально возможный размер шрифта, при котором текст укладывается
|
|
||||||
chosen_size = self._max_font_size
|
chosen_size = self._max_font_size
|
||||||
for font_size in range(self._max_font_size, self._min_font_size - 1, -1):
|
for font_size in range(self._max_font_size, self._min_font_size - 1, -1):
|
||||||
font.setPointSize(font_size)
|
font.setPointSize(font_size)
|
||||||
@@ -341,14 +358,12 @@ class AutoSizeButton(QPushButton):
|
|||||||
font.setPointSize(chosen_size)
|
font.setPointSize(chosen_size)
|
||||||
self.setFont(font)
|
self.setFont(font)
|
||||||
|
|
||||||
# После выбора шрифта вычисляем требуемую ширину для полного отображения текста
|
|
||||||
fm = QFontMetrics(font)
|
fm = QFontMetrics(font)
|
||||||
text_width = fm.horizontalAdvance(text)
|
text_width = fm.horizontalAdvance(text)
|
||||||
required_width = text_width + margins.left() + margins.right() + self._padding * 2
|
required_width = text_width + margins.left() + margins.right() + self._padding * 2
|
||||||
if self._icon:
|
if self._icon:
|
||||||
required_width += self._icon_size
|
required_width += self._icon_size
|
||||||
|
|
||||||
# Если текущая ширина меньше требуемой, обновляем минимальную ширину
|
|
||||||
if self.width() < required_width:
|
if self.width() < required_width:
|
||||||
self.setMinimumWidth(required_width)
|
self.setMinimumWidth(required_width)
|
||||||
|
|
||||||
@@ -358,7 +373,6 @@ class AutoSizeButton(QPushButton):
|
|||||||
if not self._update_size:
|
if not self._update_size:
|
||||||
return super().sizeHint()
|
return super().sizeHint()
|
||||||
else:
|
else:
|
||||||
# Вычисляем оптимальный размер кнопки на основе текста и отступов
|
|
||||||
font = self.font()
|
font = self.font()
|
||||||
fm = QFontMetrics(font)
|
fm = QFontMetrics(font)
|
||||||
text_width = fm.horizontalAdvance(self._original_text)
|
text_width = fm.horizontalAdvance(self._original_text)
|
||||||
@@ -369,7 +383,6 @@ class AutoSizeButton(QPushButton):
|
|||||||
height = fm.height() + margins.top() + margins.bottom() + self._padding
|
height = fm.height() + margins.top() + margins.bottom() + self._padding
|
||||||
return QSize(width, height)
|
return QSize(width, height)
|
||||||
|
|
||||||
|
|
||||||
class NavLabel(QLabel):
|
class NavLabel(QLabel):
|
||||||
clicked = Signal()
|
clicked = Signal()
|
||||||
|
|
||||||
@@ -381,7 +394,6 @@ class NavLabel(QLabel):
|
|||||||
self._isChecked = False
|
self._isChecked = False
|
||||||
self.setProperty("checked", self._isChecked)
|
self.setProperty("checked", self._isChecked)
|
||||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
# Explicitly enable focus
|
|
||||||
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||||
|
|
||||||
def setCheckable(self, checkable):
|
def setCheckable(self, checkable):
|
||||||
@@ -400,7 +412,6 @@ class NavLabel(QLabel):
|
|||||||
|
|
||||||
def mousePressEvent(self, event):
|
def mousePressEvent(self, event):
|
||||||
if event.button() == Qt.MouseButton.LeftButton:
|
if event.button() == Qt.MouseButton.LeftButton:
|
||||||
# Ensure widget can take focus on click
|
|
||||||
self.setFocus(Qt.FocusReason.MouseFocusReason)
|
self.setFocus(Qt.FocusReason.MouseFocusReason)
|
||||||
if self._checkable:
|
if self._checkable:
|
||||||
self.setChecked(not self._isChecked)
|
self.setChecked(not self._isChecked)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import re
|
|||||||
from typing import cast, TYPE_CHECKING
|
from typing import cast, TYPE_CHECKING
|
||||||
from PySide6.QtGui import QPixmap, QIcon
|
from PySide6.QtGui import QPixmap, QIcon
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QDialog, QFormLayout, QHBoxLayout, QLabel, QVBoxLayout, QListWidget, QScrollArea, QWidget, QListWidgetItem, QSizePolicy, QApplication
|
QDialog, QFormLayout, QHBoxLayout, QLabel, QVBoxLayout, QListWidget, QScrollArea, QWidget, QListWidgetItem, QSizePolicy, QApplication, QProgressBar
|
||||||
)
|
)
|
||||||
from PySide6.QtCore import Qt, QObject, Signal, QMimeDatabase, QTimer
|
from PySide6.QtCore import Qt, QObject, Signal, QMimeDatabase, QTimer
|
||||||
from icoextract import IconExtractor, IconExtractorError
|
from icoextract import IconExtractor, IconExtractorError
|
||||||
@@ -16,6 +16,7 @@ import portprotonqt.themes.standart.styles as default_styles
|
|||||||
from portprotonqt.theme_manager import ThemeManager
|
from portprotonqt.theme_manager import ThemeManager
|
||||||
from portprotonqt.custom_widgets import AutoSizeButton
|
from portprotonqt.custom_widgets import AutoSizeButton
|
||||||
from portprotonqt.downloader import Downloader
|
from portprotonqt.downloader import Downloader
|
||||||
|
import psutil
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from portprotonqt.main_window import MainWindow
|
from portprotonqt.main_window import MainWindow
|
||||||
@@ -89,6 +90,86 @@ def generate_thumbnail(inputfile, outfile, size=128, force_resize=True):
|
|||||||
class FileSelectedSignal(QObject):
|
class FileSelectedSignal(QObject):
|
||||||
file_selected = Signal(str) # Сигнал с путем к выбранному файлу
|
file_selected = Signal(str) # Сигнал с путем к выбранному файлу
|
||||||
|
|
||||||
|
class GameLaunchDialog(QDialog):
|
||||||
|
"""Modal dialog to indicate game launch progress, similar to Steam's launch dialog."""
|
||||||
|
def __init__(self, parent=None, game_name=None, theme=None, target_exe=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.theme = theme if theme else default_styles
|
||||||
|
self.theme_manager = ThemeManager()
|
||||||
|
self.game_name = game_name
|
||||||
|
self.target_exe = target_exe # Store the target executable name
|
||||||
|
self.setWindowTitle(_("Launching {0}").format(self.game_name))
|
||||||
|
self.setModal(True)
|
||||||
|
self.setFixedSize(400, 200)
|
||||||
|
self.setStyleSheet(self.theme.MESSAGE_BOX_STYLE)
|
||||||
|
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||||
|
self.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.FramelessWindowHint)
|
||||||
|
|
||||||
|
# Layout
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(10, 10, 10, 10)
|
||||||
|
layout.setSpacing(10)
|
||||||
|
|
||||||
|
# Game name label
|
||||||
|
label = QLabel(_("Launching {0}").format(self.game_name))
|
||||||
|
label.setStyleSheet(self.theme.PARAMS_TITLE_STYLE)
|
||||||
|
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
layout.addWidget(label)
|
||||||
|
|
||||||
|
# Progress bar (indeterminate)
|
||||||
|
self.progress_bar = QProgressBar()
|
||||||
|
self.progress_bar.setStyleSheet(self.theme.PROGRESS_BAR_STYLE)
|
||||||
|
self.progress_bar.setRange(0, 0) # Indeterminate mode
|
||||||
|
layout.addWidget(self.progress_bar)
|
||||||
|
|
||||||
|
# Cancel button
|
||||||
|
self.cancel_button = AutoSizeButton(_("Cancel"), icon=self.theme_manager.get_icon("cancel"))
|
||||||
|
self.cancel_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||||
|
self.cancel_button.clicked.connect(self.reject)
|
||||||
|
layout.addWidget(self.cancel_button, alignment=Qt.AlignmentFlag.AlignCenter)
|
||||||
|
|
||||||
|
# Center dialog on parent
|
||||||
|
if parent:
|
||||||
|
parent_geometry = parent.geometry()
|
||||||
|
center_point = parent_geometry.center()
|
||||||
|
dialog_geometry = self.geometry()
|
||||||
|
dialog_geometry.moveCenter(center_point)
|
||||||
|
self.setGeometry(dialog_geometry)
|
||||||
|
|
||||||
|
# Timer to check if the game process is running
|
||||||
|
self.check_process_timer = QTimer(self)
|
||||||
|
self.check_process_timer.timeout.connect(self.check_target_exe)
|
||||||
|
self.check_process_timer.start(500)
|
||||||
|
|
||||||
|
def is_target_exe_running(self):
|
||||||
|
"""Check if the target executable is running using psutil."""
|
||||||
|
if not self.target_exe:
|
||||||
|
return False
|
||||||
|
for proc in psutil.process_iter(attrs=["name"]):
|
||||||
|
if proc.info["name"].lower() == self.target_exe.lower():
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check_target_exe(self):
|
||||||
|
"""Check if the game process is running and close the dialog if it is."""
|
||||||
|
if self.is_target_exe_running():
|
||||||
|
logger.info(f"Game {self.game_name} process detected as running, closing launch dialog")
|
||||||
|
self.accept() # Close dialog when game is running
|
||||||
|
self.check_process_timer.stop()
|
||||||
|
self.check_process_timer.deleteLater()
|
||||||
|
elif not hasattr(self.parent(), 'game_processes') or not any(proc.poll() is None for proc in cast("MainWindow", self.parent()).game_processes):
|
||||||
|
# If no child processes are running, stop the timer but keep dialog open
|
||||||
|
self.check_process_timer.stop()
|
||||||
|
self.check_process_timer.deleteLater()
|
||||||
|
|
||||||
|
|
||||||
|
def reject(self):
|
||||||
|
"""Handle dialog cancellation."""
|
||||||
|
logger.info(f"Game launch cancelled for {self.game_name}")
|
||||||
|
self.check_process_timer.stop()
|
||||||
|
self.check_process_timer.deleteLater()
|
||||||
|
super().reject()
|
||||||
|
|
||||||
class FileExplorer(QDialog):
|
class FileExplorer(QDialog):
|
||||||
def __init__(self, parent=None, theme=None, file_filter=None, initial_path=None, directory_only=False):
|
def __init__(self, parent=None, theme=None, file_filter=None, initial_path=None, directory_only=False):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|||||||
Binary file not shown.
@@ -9,7 +9,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PROJECT VERSION\n"
|
"Project-Id-Version: PROJECT VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2025-08-26 13:17+0500\n"
|
"POT-Creation-Date: 2025-08-31 12:28+0500\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language: de_DE\n"
|
"Language: de_DE\n"
|
||||||
@@ -248,15 +248,19 @@ msgstr ""
|
|||||||
msgid "Select All"
|
msgid "Select All"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Launching {0}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Cancel"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "File Explorer"
|
msgid "File Explorer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Select"
|
msgid "Select"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Cancel"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Path: "
|
msgid "Path: "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -9,7 +9,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PROJECT VERSION\n"
|
"Project-Id-Version: PROJECT VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2025-08-26 13:17+0500\n"
|
"POT-Creation-Date: 2025-08-31 12:28+0500\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language: es_ES\n"
|
"Language: es_ES\n"
|
||||||
@@ -248,15 +248,19 @@ msgstr ""
|
|||||||
msgid "Select All"
|
msgid "Select All"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Launching {0}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Cancel"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "File Explorer"
|
msgid "File Explorer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Select"
|
msgid "Select"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Cancel"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Path: "
|
msgid "Path: "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PortProtonQt 0.1.1\n"
|
"Project-Id-Version: PortProtonQt 0.1.1\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2025-08-26 13:17+0500\n"
|
"POT-Creation-Date: 2025-08-31 12:28+0500\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -246,15 +246,19 @@ msgstr ""
|
|||||||
msgid "Select All"
|
msgid "Select All"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Launching {0}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Cancel"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "File Explorer"
|
msgid "File Explorer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Select"
|
msgid "Select"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Cancel"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Path: "
|
msgid "Path: "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -9,8 +9,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PROJECT VERSION\n"
|
"Project-Id-Version: PROJECT VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2025-08-26 13:17+0500\n"
|
"POT-Creation-Date: 2025-08-31 12:28+0500\n"
|
||||||
"PO-Revision-Date: 2025-08-26 13:16+0500\n"
|
"PO-Revision-Date: 2025-08-31 12:28+0500\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language: ru_RU\n"
|
"Language: ru_RU\n"
|
||||||
"Language-Team: ru_RU <LL@li.org>\n"
|
"Language-Team: ru_RU <LL@li.org>\n"
|
||||||
@@ -255,15 +255,19 @@ msgstr "Удалить"
|
|||||||
msgid "Select All"
|
msgid "Select All"
|
||||||
msgstr "Выбрать всё"
|
msgstr "Выбрать всё"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Launching {0}"
|
||||||
|
msgstr "Идёт запуск {0}"
|
||||||
|
|
||||||
|
msgid "Cancel"
|
||||||
|
msgstr "Отмена"
|
||||||
|
|
||||||
msgid "File Explorer"
|
msgid "File Explorer"
|
||||||
msgstr "Проводник"
|
msgstr "Проводник"
|
||||||
|
|
||||||
msgid "Select"
|
msgid "Select"
|
||||||
msgstr "Выбрать"
|
msgstr "Выбрать"
|
||||||
|
|
||||||
msgid "Cancel"
|
|
||||||
msgstr "Отмена"
|
|
||||||
|
|
||||||
msgid "Path: "
|
msgid "Path: "
|
||||||
msgstr "Путь: "
|
msgstr "Путь: "
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.6 KiB |
@@ -83,7 +83,6 @@ GAME_CARD_ANIMATION = {
|
|||||||
|
|
||||||
# Тип анимации для карточки при наведении или фокусе
|
# Тип анимации для карточки при наведении или фокусе
|
||||||
# Возможные значения: "gradient", "scale"
|
# Возможные значения: "gradient", "scale"
|
||||||
# scale крайне нестабилен и требует доработки (используйте на свой страх и риск)
|
|
||||||
# "gradient" включает вращающийся градиент для обводки, "scale" увеличивает размер карточки
|
# "gradient" включает вращающийся градиент для обводки, "scale" увеличивает размер карточки
|
||||||
"card_animation_type": "gradient",
|
"card_animation_type": "gradient",
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import sys
|
import sys
|
||||||
import subprocess
|
import subprocess
|
||||||
from PySide6.QtWidgets import QSystemTrayIcon, QMenu, QApplication
|
import shlex
|
||||||
|
import signal
|
||||||
|
import psutil
|
||||||
|
import os
|
||||||
|
from PySide6.QtWidgets import QSystemTrayIcon, QMenu, QApplication, QMessageBox
|
||||||
from PySide6.QtGui import QIcon, QAction
|
from PySide6.QtGui import QIcon, QAction
|
||||||
from PySide6.QtCore import QTimer
|
from PySide6.QtCore import QTimer
|
||||||
from portprotonqt.logger import get_logger
|
from portprotonqt.logger import get_logger
|
||||||
@@ -8,6 +12,7 @@ from portprotonqt.theme_manager import ThemeManager
|
|||||||
import portprotonqt.themes.standart.styles as default_styles
|
import portprotonqt.themes.standart.styles as default_styles
|
||||||
from portprotonqt.localization import _
|
from portprotonqt.localization import _
|
||||||
from portprotonqt.config_utils import read_favorites, read_theme_from_config, save_theme_to_config
|
from portprotonqt.config_utils import read_favorites, read_theme_from_config, save_theme_to_config
|
||||||
|
from portprotonqt.dialogs import GameLaunchDialog
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -16,18 +21,25 @@ class TrayManager:
|
|||||||
|
|
||||||
Обеспечивает:
|
Обеспечивает:
|
||||||
- Показ/скрытие главного окна по двойному клику на иконку трея.
|
- Показ/скрытие главного окна по двойному клику на иконку трея.
|
||||||
- Контекстное меню с опциями: Show/Hide (переключается в зависимости от состояния окна),
|
- Контекстное меню с опциями: Show/Hide, Favorites, Recent Games, Themes, Exit.
|
||||||
Favorites (быстрый запуск избранных игр), Recent Games (быстрый запуск недавних игр), Themes (быстрая смена тем), Exit.
|
- Динамическое заполнение меню Favorites, Recent Games и Themes.
|
||||||
- Меню Favorites, Recent Games и Themes динамически заполняются при показе (через aboutToShow).
|
- Сворачивание в трей при закрытии окна, полное закрытие через Exit.
|
||||||
- При закрытии окна (крестик) приложение сворачивается в трей, а не закрывается.
|
|
||||||
Полное закрытие только через Exit в меню.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, main_window, app_name: str | None = None, theme=None):
|
def __init__(self, main_window, app_name: str | None = None, theme=None):
|
||||||
self.app_name = app_name if app_name is not None else "PortProtonQt"
|
self.app_name = app_name if app_name is not None else "PortProtonQt"
|
||||||
self.theme_manager = ThemeManager()
|
self.theme_manager = ThemeManager()
|
||||||
self.theme = theme if theme is not None else default_styles
|
selected_theme = read_theme_from_config()
|
||||||
self.current_theme_name = read_theme_from_config()
|
self.current_theme_name = selected_theme
|
||||||
|
try:
|
||||||
|
self.theme = self.theme_manager.apply_theme(selected_theme)
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.warning(f"Тема '{selected_theme}' не найдена, применяется стандартная тема 'standart'")
|
||||||
|
self.theme = self.theme_manager.apply_theme("standart")
|
||||||
|
self.current_theme_name = "standart"
|
||||||
|
save_theme_to_config("standart")
|
||||||
|
if not self.theme:
|
||||||
|
self.theme = default_styles
|
||||||
self.main_window = main_window
|
self.main_window = main_window
|
||||||
self.tray_icon = QSystemTrayIcon(self.main_window)
|
self.tray_icon = QSystemTrayIcon(self.main_window)
|
||||||
|
|
||||||
@@ -41,24 +53,19 @@ class TrayManager:
|
|||||||
self.tray_icon.activated.connect(self.handle_tray_click)
|
self.tray_icon.activated.connect(self.handle_tray_click)
|
||||||
self.tray_icon.setToolTip(self.app_name)
|
self.tray_icon.setToolTip(self.app_name)
|
||||||
|
|
||||||
# Контекстное меню
|
|
||||||
self.tray_menu = QMenu()
|
self.tray_menu = QMenu()
|
||||||
self.toggle_action = QAction(_("Show"), self.main_window)
|
self.toggle_action = QAction(_("Show"), self.main_window)
|
||||||
self.toggle_action.triggered.connect(self.toggle_window_action)
|
self.toggle_action.triggered.connect(self.toggle_window_action)
|
||||||
|
|
||||||
# Подменю для избранных игр
|
|
||||||
self.favorites_menu = QMenu(_("Favorites"))
|
self.favorites_menu = QMenu(_("Favorites"))
|
||||||
self.favorites_menu.aboutToShow.connect(self.populate_favorites_menu)
|
self.favorites_menu.aboutToShow.connect(self.populate_favorites_menu)
|
||||||
|
|
||||||
# Подменю для недавних игр
|
|
||||||
self.recent_menu = QMenu(_("Recent Games"))
|
self.recent_menu = QMenu(_("Recent Games"))
|
||||||
self.recent_menu.aboutToShow.connect(self.populate_recent_menu)
|
self.recent_menu.aboutToShow.connect(self.populate_recent_menu)
|
||||||
|
|
||||||
# Подменю для тем
|
|
||||||
self.themes_menu = QMenu(_("Themes"))
|
self.themes_menu = QMenu(_("Themes"))
|
||||||
self.themes_menu.aboutToShow.connect(self.populate_themes_menu)
|
self.themes_menu.aboutToShow.connect(self.populate_themes_menu)
|
||||||
|
|
||||||
# Добавляем действия в меню
|
|
||||||
self.tray_menu.addAction(self.toggle_action)
|
self.tray_menu.addAction(self.toggle_action)
|
||||||
self.tray_menu.addSeparator()
|
self.tray_menu.addSeparator()
|
||||||
self.tray_menu.addMenu(self.favorites_menu)
|
self.tray_menu.addMenu(self.favorites_menu)
|
||||||
@@ -74,41 +81,35 @@ class TrayManager:
|
|||||||
self.tray_icon.setContextMenu(self.tray_menu)
|
self.tray_icon.setContextMenu(self.tray_menu)
|
||||||
self.tray_icon.show()
|
self.tray_icon.show()
|
||||||
|
|
||||||
# Флаг для принудительного выхода
|
|
||||||
self.main_window.is_exiting = False
|
self.main_window.is_exiting = False
|
||||||
|
|
||||||
# Переменные для отслеживания двойного клика
|
|
||||||
self.click_count = 0
|
self.click_count = 0
|
||||||
self.click_timer = QTimer()
|
self.click_timer = QTimer()
|
||||||
self.click_timer.setSingleShot(True)
|
self.click_timer.setSingleShot(True)
|
||||||
self.click_timer.timeout.connect(self.reset_click_count)
|
self.click_timer.timeout.connect(self.reset_click_count)
|
||||||
|
|
||||||
|
self.launch_dialog = None
|
||||||
|
|
||||||
def update_toggle_action(self):
|
def update_toggle_action(self):
|
||||||
"""Update toggle_action text based on window visibility."""
|
|
||||||
if self.main_window.isVisible():
|
if self.main_window.isVisible():
|
||||||
self.toggle_action.setText(_("Hide"))
|
self.toggle_action.setText(_("Hide"))
|
||||||
else:
|
else:
|
||||||
self.toggle_action.setText(_("Show"))
|
self.toggle_action.setText(_("Show"))
|
||||||
|
|
||||||
def handle_tray_click(self, reason):
|
def handle_tray_click(self, reason):
|
||||||
"""Обрабатывает клики по иконке трея, отслеживая двойной клик."""
|
|
||||||
if reason == QSystemTrayIcon.ActivationReason.Trigger:
|
if reason == QSystemTrayIcon.ActivationReason.Trigger:
|
||||||
self.click_count += 1
|
self.click_count += 1
|
||||||
if self.click_count == 1:
|
if self.click_count == 1:
|
||||||
# Запускаем таймер для ожидания второго клика (300 мс - стандартное время для двойного клика)
|
|
||||||
self.click_timer.start(300)
|
self.click_timer.start(300)
|
||||||
elif self.click_count == 2:
|
elif self.click_count == 2:
|
||||||
# Двойной клик зафиксирован
|
|
||||||
self.click_timer.stop()
|
self.click_timer.stop()
|
||||||
self.toggle_window_action()
|
self.toggle_window_action()
|
||||||
self.click_count = 0
|
self.click_count = 0
|
||||||
|
|
||||||
def reset_click_count(self):
|
def reset_click_count(self):
|
||||||
"""Сбрасывает счетчик кликов, если таймер истек."""
|
|
||||||
self.click_count = 0
|
self.click_count = 0
|
||||||
|
|
||||||
def toggle_window_action(self):
|
def toggle_window_action(self):
|
||||||
"""Toggle window visibility and update action text."""
|
|
||||||
if self.main_window.isVisible():
|
if self.main_window.isVisible():
|
||||||
self.main_window.hide()
|
self.main_window.hide()
|
||||||
else:
|
else:
|
||||||
@@ -117,7 +118,6 @@ class TrayManager:
|
|||||||
self.main_window.activateWindow()
|
self.main_window.activateWindow()
|
||||||
|
|
||||||
def populate_favorites_menu(self):
|
def populate_favorites_menu(self):
|
||||||
"""Динамически заполняет меню избранных игр с указанием источника."""
|
|
||||||
self.favorites_menu.clear()
|
self.favorites_menu.clear()
|
||||||
favorites = read_favorites()
|
favorites = read_favorites()
|
||||||
if not favorites:
|
if not favorites:
|
||||||
@@ -134,13 +134,12 @@ class TrayManager:
|
|||||||
exec_line, source = game_data
|
exec_line, source = game_data
|
||||||
action_text = f"{fav} ({source})"
|
action_text = f"{fav} ({source})"
|
||||||
action = QAction(action_text, self.main_window)
|
action = QAction(action_text, self.main_window)
|
||||||
action.triggered.connect(lambda checked=False, el=exec_line: self.main_window.toggleGame(el))
|
action.triggered.connect(lambda checked=False, el=exec_line, name=fav: self.launch_game_with_dialog(el, name))
|
||||||
self.favorites_menu.addAction(action)
|
self.favorites_menu.addAction(action)
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Exec line not found for favorite: {fav}")
|
logger.warning(f"Exec line not found for favorite: {fav}")
|
||||||
|
|
||||||
def populate_recent_menu(self):
|
def populate_recent_menu(self):
|
||||||
"""Динамически заполняет меню недавних игр (топ-5 по timestamp) с указанием источника."""
|
|
||||||
self.recent_menu.clear()
|
self.recent_menu.clear()
|
||||||
if not self.main_window.games:
|
if not self.main_window.games:
|
||||||
no_recent_action = QAction(_("No recent games"), self.main_window)
|
no_recent_action = QAction(_("No recent games"), self.main_window)
|
||||||
@@ -156,45 +155,99 @@ class TrayManager:
|
|||||||
source = game[12]
|
source = game[12]
|
||||||
action_text = f"{game_name} ({source})"
|
action_text = f"{game_name} ({source})"
|
||||||
action = QAction(action_text, self.main_window)
|
action = QAction(action_text, self.main_window)
|
||||||
action.triggered.connect(lambda checked=False, el=exec_line: self.main_window.toggleGame(el))
|
action.triggered.connect(lambda checked=False, el=exec_line, name=game_name: self.launch_game_with_dialog(el, name))
|
||||||
self.recent_menu.addAction(action)
|
self.recent_menu.addAction(action)
|
||||||
|
|
||||||
|
def launch_game_with_dialog(self, exec_line, game_name):
|
||||||
|
"""Launch a game with a modal dialog indicating progress."""
|
||||||
|
try:
|
||||||
|
# Determine target executable
|
||||||
|
target_exe = None
|
||||||
|
if exec_line.startswith("steam://"):
|
||||||
|
# Steam games are handled differently, no target_exe needed
|
||||||
|
self.launch_dialog = GameLaunchDialog(self.main_window, game_name=game_name, theme=self.theme)
|
||||||
|
else:
|
||||||
|
# Extract target executable from exec_line
|
||||||
|
entry_exec_split = shlex.split(exec_line)
|
||||||
|
if entry_exec_split[0] == "env" and len(entry_exec_split) > 2:
|
||||||
|
file_to_check = entry_exec_split[2]
|
||||||
|
elif entry_exec_split[0] == "flatpak" and len(entry_exec_split) > 3:
|
||||||
|
file_to_check = entry_exec_split[3]
|
||||||
|
else:
|
||||||
|
file_to_check = entry_exec_split[0]
|
||||||
|
|
||||||
|
if not os.path.exists(file_to_check):
|
||||||
|
logger.error(f"File not found: {file_to_check}")
|
||||||
|
QMessageBox.warning(self.main_window, _("Error"), _("File not found: {0}").format(file_to_check))
|
||||||
|
return
|
||||||
|
|
||||||
|
target_exe = os.path.basename(file_to_check)
|
||||||
|
self.launch_dialog = GameLaunchDialog(self.main_window, game_name=game_name, theme=self.theme, target_exe=target_exe)
|
||||||
|
|
||||||
|
self.launch_dialog.rejected.connect(lambda: self.cancel_game_launch(exec_line))
|
||||||
|
self.launch_dialog.show()
|
||||||
|
|
||||||
|
self.main_window.toggleGame(exec_line)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to launch game {game_name}: {e}")
|
||||||
|
if self.launch_dialog:
|
||||||
|
self.launch_dialog.reject()
|
||||||
|
self.launch_dialog = None
|
||||||
|
QMessageBox.warning(self.main_window, _("Error"), _("Failed to launch game: {0}").format(str(e)))
|
||||||
|
|
||||||
|
def cancel_game_launch(self, exec_line):
|
||||||
|
"""Cancel the game launch and terminate the process, using MainWindow's stop logic."""
|
||||||
|
if self.main_window.game_processes and self.main_window.target_exe:
|
||||||
|
for proc in self.main_window.game_processes:
|
||||||
|
try:
|
||||||
|
parent = psutil.Process(proc.pid)
|
||||||
|
children = parent.children(recursive=True)
|
||||||
|
for child in children:
|
||||||
|
try:
|
||||||
|
child.terminate()
|
||||||
|
except psutil.NoSuchProcess:
|
||||||
|
pass
|
||||||
|
psutil.wait_procs(children, timeout=5)
|
||||||
|
for child in children:
|
||||||
|
if child.is_running():
|
||||||
|
child.kill()
|
||||||
|
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
|
||||||
|
except psutil.NoSuchProcess:
|
||||||
|
pass
|
||||||
|
self.main_window.game_processes = []
|
||||||
|
self.main_window.resetPlayButton()
|
||||||
|
if self.launch_dialog:
|
||||||
|
self.launch_dialog.reject()
|
||||||
|
self.launch_dialog = None
|
||||||
|
logger.info(f"Game launch cancelled for exec line: {exec_line}")
|
||||||
|
|
||||||
def populate_themes_menu(self):
|
def populate_themes_menu(self):
|
||||||
"""Динамически заполняет меню тем, позволяя переключать доступные темы."""
|
|
||||||
self.themes_menu.clear()
|
self.themes_menu.clear()
|
||||||
available_themes = self.theme_manager.get_available_themes()
|
available_themes = self.theme_manager.get_available_themes()
|
||||||
current_theme = read_theme_from_config()
|
|
||||||
|
|
||||||
for theme_name in sorted(available_themes):
|
for theme_name in sorted(available_themes):
|
||||||
action = QAction(theme_name, self.main_window)
|
action = QAction(theme_name, self.main_window)
|
||||||
action.setCheckable(True)
|
action.setCheckable(True)
|
||||||
action.setChecked(theme_name == current_theme)
|
action.setChecked(theme_name == self.current_theme_name)
|
||||||
action.triggered.connect(lambda checked=False, tn=theme_name: self.switch_theme(tn))
|
action.triggered.connect(lambda checked=False, tn=theme_name: self.switch_theme(tn))
|
||||||
self.themes_menu.addAction(action)
|
self.themes_menu.addAction(action)
|
||||||
|
|
||||||
def switch_theme(self, theme_name: str):
|
def switch_theme(self, theme_name: str):
|
||||||
"""Сохраняет выбранную тему и перезапускает приложение для применения изменений."""
|
|
||||||
try:
|
try:
|
||||||
# Сохраняем новую тему в конфигурации
|
|
||||||
save_theme_to_config(theme_name)
|
save_theme_to_config(theme_name)
|
||||||
logger.info(f"Saved theme {theme_name}, restarting application to apply changes")
|
logger.info(f"Saved theme {theme_name}, restarting application to apply changes")
|
||||||
|
|
||||||
# Получаем текущий исполняемый файл и аргументы
|
|
||||||
executable = sys.executable
|
executable = sys.executable
|
||||||
args = sys.argv
|
args = sys.argv
|
||||||
|
|
||||||
# Закрываем текущее приложение
|
|
||||||
self.main_window.is_exiting = True
|
self.main_window.is_exiting = True
|
||||||
QApplication.quit()
|
QApplication.quit()
|
||||||
|
|
||||||
# Перезапускаем приложение
|
|
||||||
subprocess.Popen([executable] + args)
|
subprocess.Popen([executable] + args)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to switch theme to {theme_name}: {e}")
|
logger.error(f"Failed to switch theme to {theme_name}: {e}")
|
||||||
# В случае ошибки сохраняем стандартную тему
|
|
||||||
save_theme_to_config("standart")
|
save_theme_to_config("standart")
|
||||||
# Перезапускаем приложение с дефолтной темой
|
|
||||||
executable = sys.executable
|
executable = sys.executable
|
||||||
args = sys.argv
|
args = sys.argv
|
||||||
self.main_window.is_exiting = True
|
self.main_window.is_exiting = True
|
||||||
@@ -202,7 +255,6 @@ class TrayManager:
|
|||||||
subprocess.Popen([executable] + args)
|
subprocess.Popen([executable] + args)
|
||||||
|
|
||||||
def force_exit(self):
|
def force_exit(self):
|
||||||
"""Принудительно закрывает приложение."""
|
|
||||||
self.main_window.is_exiting = True
|
self.main_window.is_exiting = True
|
||||||
self.main_window.close()
|
self.main_window.close()
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "portprotonqt"
|
name = "portprotonqt"
|
||||||
version = "0.1.4"
|
version = "0.1.5"
|
||||||
description = "A project to rewrite PortProton (PortWINE) using PySide"
|
description = "A project to rewrite PortProton (PortWINE) using PySide"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = { text = "GPL-3.0" }
|
license = { text = "GPL-3.0" }
|
||||||
|
|||||||
Reference in New Issue
Block a user