forked from Boria138/PortProtonQt
feat(wine settings): make winetricks work
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
@@ -2,11 +2,13 @@ import os
|
||||
import tempfile
|
||||
import re
|
||||
from typing import cast, TYPE_CHECKING
|
||||
from PySide6.QtGui import QPixmap, QIcon
|
||||
from PySide6.QtGui import QPixmap, QIcon, QTextCursor
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QFormLayout, QHBoxLayout, QLabel, QVBoxLayout, QListWidget, QScrollArea, QWidget, QListWidgetItem, QSizePolicy, QApplication, QProgressBar, QScroller
|
||||
QDialog, QFormLayout, QHBoxLayout, QLabel, QVBoxLayout, QListWidget, QScrollArea, QWidget, QListWidgetItem, QSizePolicy, QApplication, QProgressBar, QScroller,
|
||||
QTabWidget, QTableWidget, QHeaderView, QMessageBox, QTableWidgetItem, QTextEdit
|
||||
)
|
||||
from PySide6.QtCore import Qt, QObject, Signal, QMimeDatabase, QTimer, QThreadPool, QRunnable, Slot
|
||||
|
||||
from PySide6.QtCore import Qt, QObject, Signal, QMimeDatabase, QTimer, QThreadPool, QRunnable, Slot, QProcess, QProcessEnvironment
|
||||
from icoextract import IconExtractor, IconExtractorError
|
||||
from PIL import Image
|
||||
from portprotonqt.config_utils import get_portproton_location, read_favorite_folders, read_theme_from_config
|
||||
@@ -977,3 +979,379 @@ Icon={icon_path}
|
||||
"""
|
||||
|
||||
return desktop_entry, desktop_path
|
||||
|
||||
class WinetricksDialog(QDialog):
|
||||
"""Dialog for managing Winetricks components in a prefix."""
|
||||
|
||||
def __init__(self, parent=None, theme=None, prefix_path: str | None = None, wine_use: str | None = None):
|
||||
super().__init__(parent)
|
||||
self.theme = theme if theme else theme_manager.apply_theme(read_theme_from_config())
|
||||
self.prefix_path: str | None = prefix_path
|
||||
self.wine_use: str | None = wine_use
|
||||
self.portproton_path = get_portproton_location()
|
||||
if self.portproton_path is None:
|
||||
logger.error("PortProton location not found")
|
||||
return
|
||||
self.tmp_path = os.path.join(self.portproton_path, "data", "tmp")
|
||||
os.makedirs(self.tmp_path, exist_ok=True)
|
||||
self.winetricks_path = os.path.join(self.tmp_path, "winetricks")
|
||||
if self.prefix_path is None:
|
||||
logger.error("Prefix path not provided")
|
||||
return
|
||||
self.log_path = os.path.join(self.prefix_path, "winetricks.log")
|
||||
os.makedirs(os.path.dirname(self.log_path), exist_ok=True)
|
||||
if not os.path.exists(self.log_path):
|
||||
open(self.log_path, 'a').close()
|
||||
|
||||
self.downloader = Downloader(max_workers=4)
|
||||
self.apply_process: QProcess | None = None
|
||||
|
||||
self.setWindowTitle(_("Prefix Manager"))
|
||||
self.setModal(True)
|
||||
self.resize(700, 700)
|
||||
self.setStyleSheet(self.theme.MAIN_WINDOW_STYLE + self.theme.MESSAGE_BOX_STYLE)
|
||||
|
||||
self.update_winetricks()
|
||||
self.setup_ui()
|
||||
self.load_lists()
|
||||
|
||||
def update_winetricks(self):
|
||||
"""Update the winetricks script."""
|
||||
if not self.downloader.has_internet():
|
||||
logger.warning("No internet connection, skipping winetricks update")
|
||||
return
|
||||
|
||||
url = "https://raw.githubusercontent.com/Winetricks/winetricks/master/src/winetricks"
|
||||
temp_path = os.path.join(self.tmp_path, "winetricks_temp")
|
||||
|
||||
try:
|
||||
self.downloader.download(url, temp_path)
|
||||
with open(temp_path) as f:
|
||||
ext_content = f.read()
|
||||
ext_ver_match = re.search(r'WINETRICKS_VERSION\s*=\s*[\'"]?([^\'"\s]+)', ext_content)
|
||||
ext_ver = ext_ver_match.group(1) if ext_ver_match else None
|
||||
logger.info(f"External winetricks version: {ext_ver}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get external version: {e}")
|
||||
ext_ver = None
|
||||
if os.path.exists(temp_path):
|
||||
os.remove(temp_path)
|
||||
return
|
||||
|
||||
int_ver = None
|
||||
if os.path.exists(self.winetricks_path):
|
||||
try:
|
||||
with open(self.winetricks_path) as f:
|
||||
int_content = f.read()
|
||||
int_ver_match = re.search(r'WINETRICKS_VERSION\s*=\s*[\'"]?([^\'"\s]+)', int_content)
|
||||
int_ver = int_ver_match.group(1) if int_ver_match else None
|
||||
logger.info(f"Internal winetricks version: {int_ver}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to read internal winetricks version: {e}")
|
||||
|
||||
update_needed = not os.path.exists(self.winetricks_path) or (int_ver != ext_ver and ext_ver)
|
||||
|
||||
if update_needed:
|
||||
try:
|
||||
self.downloader.download(url, self.winetricks_path)
|
||||
os.chmod(self.winetricks_path, 0o755)
|
||||
logger.info(f"Winetricks updated to version {ext_ver}")
|
||||
self.apply_modifications(self.winetricks_path)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update winetricks: {e}")
|
||||
elif os.path.exists(self.winetricks_path):
|
||||
self.apply_modifications(self.winetricks_path)
|
||||
|
||||
if os.path.exists(temp_path):
|
||||
os.remove(temp_path)
|
||||
|
||||
def apply_modifications(self, file_path):
|
||||
"""Apply custom modifications to the winetricks script."""
|
||||
if not os.path.exists(file_path):
|
||||
return
|
||||
try:
|
||||
with open(file_path) as f:
|
||||
content = f.read()
|
||||
|
||||
# Apply sed-like replacements
|
||||
content = re.sub(r'w_metadata vcrun2015 dlls \\', r'w_metadata !dont_use_2015! dlls \\', content)
|
||||
content = re.sub(r'w_metadata vcrun2017 dlls \\', r'w_metadata !dont_use_2017! dlls \\', content)
|
||||
content = re.sub(r'w_metadata vcrun2019 dlls \\', r'w_metadata !dont_use_2019! dlls \\', content)
|
||||
content = re.sub(r'w_set_winver win2k3', r'w_set_winver win7', content)
|
||||
|
||||
with open(file_path, 'w') as f:
|
||||
f.write(content)
|
||||
logger.info("Winetricks modifications applied")
|
||||
except Exception as e:
|
||||
logger.error(f"Error applying modifications to winetricks: {e}")
|
||||
|
||||
def setup_ui(self):
|
||||
"""Set up the user interface with tabs and tables."""
|
||||
main_layout = QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(10, 10, 10, 10)
|
||||
main_layout.setSpacing(10)
|
||||
|
||||
# Log output
|
||||
self.log_output = QTextEdit()
|
||||
self.log_output.setReadOnly(True)
|
||||
self.log_output.setStyleSheet(self.theme.WINETRICKS_LOG_STYLE)
|
||||
main_layout.addWidget(self.log_output)
|
||||
|
||||
# Tab widget
|
||||
self.tab_widget = QTabWidget()
|
||||
self.tab_widget.setStyleSheet(self.theme.WINETRICKS_TAB_STYLE)
|
||||
|
||||
table_base_style = self.theme.WINETRICKS_TABBLE_STYLE
|
||||
|
||||
# DLLs tab
|
||||
self.dll_table = QTableWidget()
|
||||
self.dll_table.setColumnCount(3)
|
||||
self.dll_table.setHorizontalHeaderLabels([_("Set"), _("Libraries"), _("Information")])
|
||||
self.dll_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
|
||||
self.dll_table.horizontalHeader().resizeSection(0, 50)
|
||||
self.dll_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
||||
self.dll_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
|
||||
self.dll_table.setStyleSheet(table_base_style)
|
||||
self.tab_widget.addTab(self.dll_table, _("DLLs"))
|
||||
|
||||
# Fonts tab
|
||||
self.fonts_table = QTableWidget()
|
||||
self.fonts_table.setColumnCount(3)
|
||||
self.fonts_table.setHorizontalHeaderLabels([_("Set"), _("Fonts"), _("Information")])
|
||||
self.fonts_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
|
||||
self.fonts_table.horizontalHeader().resizeSection(0, 50)
|
||||
self.fonts_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
||||
self.fonts_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
|
||||
self.fonts_table.setStyleSheet(table_base_style)
|
||||
self.tab_widget.addTab(self.fonts_table, _("Fonts"))
|
||||
|
||||
# Settings tab
|
||||
self.settings_table = QTableWidget()
|
||||
self.settings_table.setColumnCount(3)
|
||||
self.settings_table.setHorizontalHeaderLabels([_("Set"), _("Settings"), _("Information")])
|
||||
self.settings_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
|
||||
self.settings_table.horizontalHeader().resizeSection(0, 50)
|
||||
self.settings_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
||||
self.settings_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
|
||||
self.settings_table.setStyleSheet(table_base_style)
|
||||
self.tab_widget.addTab(self.settings_table, _("Settings"))
|
||||
|
||||
main_layout.addWidget(self.tab_widget)
|
||||
|
||||
# Buttons
|
||||
button_layout = QHBoxLayout()
|
||||
button_layout.setSpacing(10)
|
||||
self.cancel_button = AutoSizeButton(_("Cancel"), icon=theme_manager.get_icon("cancel"))
|
||||
self.force_button = AutoSizeButton(_("Force Install"), theme_manager.get_icon("apply"))
|
||||
self.install_button = AutoSizeButton(_("Install"), icon=theme_manager.get_icon("apply"))
|
||||
self.cancel_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||
self.force_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||
self.install_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||
button_layout.addWidget(self.cancel_button)
|
||||
button_layout.addWidget(self.force_button)
|
||||
button_layout.addWidget(self.install_button)
|
||||
main_layout.addLayout(button_layout)
|
||||
|
||||
self.cancel_button.clicked.connect(self.reject)
|
||||
self.force_button.clicked.connect(lambda: self.install_selected(force=True))
|
||||
self.install_button.clicked.connect(lambda: self.install_selected(force=False))
|
||||
|
||||
def load_lists(self):
|
||||
"""Load and populate the lists for DLLs, Fonts, and Settings"""
|
||||
if not os.path.exists(self.winetricks_path):
|
||||
QMessageBox.warning(self, _("Error"), _("Winetricks not found. Please try again."))
|
||||
self.reject()
|
||||
return
|
||||
|
||||
assert self.prefix_path is not None
|
||||
env = QProcessEnvironment.systemEnvironment()
|
||||
env.insert("WINEPREFIX", self.prefix_path)
|
||||
if self.wine_use is not None:
|
||||
env.insert("WINE", self.wine_use)
|
||||
|
||||
cwd = os.path.dirname(self.winetricks_path)
|
||||
|
||||
# DLLs
|
||||
self._start_list_process("dlls", self.dll_table, self.get_dll_exclusions(), env, cwd)
|
||||
|
||||
# Fonts
|
||||
self._start_list_process("fonts", self.fonts_table, self.get_fonts_exclusions(), env, cwd)
|
||||
|
||||
# Settings
|
||||
self._start_list_process("settings", self.settings_table, self.get_settings_exclusions(), env, cwd)
|
||||
|
||||
|
||||
def _start_list_process(self, category, table, exclusion_pattern, env, cwd):
|
||||
"""Запускает QProcess для списка."""
|
||||
process = QProcess(self)
|
||||
process.setProcessChannelMode(QProcess.ProcessChannelMode.MergedChannels)
|
||||
process.setProcessEnvironment(env)
|
||||
process.finished.connect(lambda exit_code, exit_status: self._on_list_finished(category, table, exclusion_pattern, process, exit_code, exit_status))
|
||||
process.start(self.winetricks_path, [category, "list"])
|
||||
|
||||
def _on_list_finished(self, category, table, exclusion_pattern, process: QProcess | None, exit_code, exit_status):
|
||||
"""Обработчик завершения списка."""
|
||||
if process is None:
|
||||
logger.error(f"Process is None for {category}")
|
||||
return
|
||||
output = bytes(process.readAllStandardOutput().data()).decode('utf-8', 'ignore')
|
||||
if exit_code == 0 and exit_status == QProcess.ExitStatus.NormalExit:
|
||||
self.populate_table(table, output, exclusion_pattern, self.log_path)
|
||||
else:
|
||||
error_output = bytes(process.readAllStandardError().data()).decode('utf-8', 'ignore')
|
||||
logger.error(f"Failed to list {category}: {error_output}")
|
||||
|
||||
def get_dll_exclusions(self):
|
||||
"""Get regex pattern for DLL exclusions."""
|
||||
return r'(d3d|directx9|dont_use|dxvk|vkd3d|galliumnine|faudio1|Foundation)'
|
||||
|
||||
def get_fonts_exclusions(self):
|
||||
"""Get regex pattern for Fonts exclusions."""
|
||||
return r'dont_use'
|
||||
|
||||
def get_settings_exclusions(self):
|
||||
"""Get regex pattern for Settings exclusions."""
|
||||
return r'(vista|alldlls|autostart_|bad|good|win|videomemory|vd=|isolate_home)'
|
||||
|
||||
def populate_table(self, table, output, exclusion_pattern, log_path):
|
||||
"""Populate the table with items from output, checking installation status."""
|
||||
table.setRowCount(0)
|
||||
table.verticalHeader().setVisible(False)
|
||||
lines = output.strip().split('\n')
|
||||
installed = set()
|
||||
if os.path.exists(log_path):
|
||||
with open(log_path) as f:
|
||||
for line in f:
|
||||
installed.add(line.strip())
|
||||
|
||||
# regex-парсинг (имя - первое слово, остальное - описание)
|
||||
line_re = re.compile(r"^\s*(?:\[(.)]\s+)?([^\s]+)\s*(.*)")
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line or re.search(exclusion_pattern, line, re.I):
|
||||
continue
|
||||
|
||||
line = line.split('(', 1)[0].strip()
|
||||
|
||||
match = line_re.match(line)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
_status, name, info = match.groups()
|
||||
# Очищаем info от мусора
|
||||
info = re.sub(r'\[.*?\]', '', info).strip() # Удаляем [скачивания] и т.п.
|
||||
|
||||
# To match bash desc extraction: after name, substr(2) to trim leading space
|
||||
if info.startswith(' '):
|
||||
info = info[1:].lstrip()
|
||||
|
||||
# Фильтр служебных строк
|
||||
if '/' in name or '\\' in name or name.lower() in ('executing', 'using', 'warning:') or name.endswith(':'):
|
||||
continue
|
||||
|
||||
checked = Qt.CheckState.Checked if name in installed else Qt.CheckState.Unchecked
|
||||
|
||||
row = table.rowCount()
|
||||
table.insertRow(row)
|
||||
|
||||
# Checkbox
|
||||
checkbox = QTableWidgetItem()
|
||||
checkbox.setCheckState(checked)
|
||||
table.setItem(row, 0, checkbox)
|
||||
|
||||
# Name
|
||||
name_item = QTableWidgetItem(name)
|
||||
table.setItem(row, 1, name_item)
|
||||
|
||||
# Info
|
||||
info_item = QTableWidgetItem(info)
|
||||
table.setItem(row, 2, info_item)
|
||||
|
||||
def install_selected(self, force=False):
|
||||
"""Install selected components."""
|
||||
selected = []
|
||||
for table in [self.dll_table, self.fonts_table, self.settings_table]:
|
||||
for row in range(table.rowCount()):
|
||||
checkbox = table.item(row, 0)
|
||||
if checkbox is not None and checkbox.checkState() == Qt.CheckState.Checked:
|
||||
name_item = table.item(row, 1)
|
||||
if name_item is not None:
|
||||
name = name_item.text()
|
||||
if name and name not in selected:
|
||||
selected.append(name)
|
||||
|
||||
if not selected:
|
||||
QMessageBox.information(self, _("Info"), _("No components selected."))
|
||||
return
|
||||
|
||||
# Load installed
|
||||
installed = set()
|
||||
if os.path.exists(self.log_path):
|
||||
with open(self.log_path) as f:
|
||||
for line in f:
|
||||
installed.add(line.strip())
|
||||
|
||||
# Filter to new selected
|
||||
new_selected = [name for name in selected if name not in installed]
|
||||
|
||||
if not new_selected:
|
||||
QMessageBox.information(self, _("Info"), _("No new components selected."))
|
||||
return
|
||||
|
||||
self.install_button.setEnabled(False)
|
||||
self.force_button.setEnabled(False)
|
||||
self.cancel_button.setEnabled(False)
|
||||
|
||||
self._start_install_process(new_selected, force)
|
||||
|
||||
def _start_install_process(self, selected, force):
|
||||
"""Запускает QProcess для установки."""
|
||||
assert self.prefix_path is not None
|
||||
env = QProcessEnvironment.systemEnvironment()
|
||||
env.insert("WINEPREFIX", self.prefix_path)
|
||||
if self.wine_use is not None:
|
||||
env.insert("WINE", self.wine_use)
|
||||
|
||||
self.apply_process = QProcess(self)
|
||||
self.apply_process.setProcessChannelMode(QProcess.ProcessChannelMode.MergedChannels)
|
||||
self.apply_process.setProcessEnvironment(env)
|
||||
self.apply_process.readyReadStandardOutput.connect(self._on_ready_read)
|
||||
self.apply_process.finished.connect(lambda exit_code, exit_status: self._on_install_finished(exit_code, exit_status, selected))
|
||||
args = ["--unattended"] + (["--force"] if force else []) + selected
|
||||
self.apply_process.start(self.winetricks_path, args)
|
||||
|
||||
def _on_ready_read(self):
|
||||
"""Handle ready read for install process."""
|
||||
if self.apply_process is None:
|
||||
return
|
||||
data = self.apply_process.readAllStandardOutput().data()
|
||||
message = bytes(data).decode('utf-8', 'ignore').strip()
|
||||
self._log(message)
|
||||
|
||||
def _on_install_finished(self, exit_code, exit_status, selected):
|
||||
"""Обработчик завершения установки."""
|
||||
error_message = ""
|
||||
if self.apply_process is not None:
|
||||
error_data = self.apply_process.readAllStandardError().data()
|
||||
error_message = bytes(error_data).decode('utf-8', 'ignore')
|
||||
if exit_code != 0 or exit_status != QProcess.ExitStatus.NormalExit:
|
||||
logger.error(f"Winetricks install failed: {error_message}")
|
||||
QMessageBox.warning(self, _("Error"), _("Installation failed. Check logs."))
|
||||
else:
|
||||
with open(self.log_path, 'a') as f:
|
||||
for name in selected:
|
||||
f.write(f"{name}\n")
|
||||
logger.info("Winetricks installation completed successfully.")
|
||||
QMessageBox.information(self, _("Success"), _("Components installed successfully."))
|
||||
self.load_lists()
|
||||
|
||||
# Разблокировка
|
||||
self.install_button.setEnabled(True)
|
||||
self.force_button.setEnabled(True)
|
||||
self.cancel_button.setEnabled(True)
|
||||
|
||||
def _log(self, message):
|
||||
"""Добавляет в лог."""
|
||||
self.log_output.append(message)
|
||||
self.log_output.moveCursor(QTextCursor.MoveOperation.End)
|
||||
|
@@ -7,7 +7,7 @@ import sys
|
||||
import psutil
|
||||
|
||||
from portprotonqt.logger import get_logger
|
||||
from portprotonqt.dialogs import AddGameDialog, FileExplorer
|
||||
from portprotonqt.dialogs import AddGameDialog, FileExplorer, WinetricksDialog
|
||||
from portprotonqt.game_card import GameCard
|
||||
from portprotonqt.animations import DetailPageAnimations
|
||||
from portprotonqt.custom_widgets import ClickableLabel, AutoSizeButton, NavLabel
|
||||
@@ -1079,7 +1079,7 @@ class MainWindow(QMainWindow):
|
||||
additional_grid.setSpacing(6)
|
||||
|
||||
additional_buttons = [
|
||||
("Winetricks", None),
|
||||
("Winetricks", self.open_winetricks),
|
||||
(_("Create Prefix Backup"), self.create_prefix_backup),
|
||||
(_("Load Prefix Backup"), self.load_prefix_backup),
|
||||
(_("Delete Compatibility Tool"), self.delete_compat_tool),
|
||||
@@ -1226,6 +1226,24 @@ class MainWindow(QMainWindow):
|
||||
except Exception as e:
|
||||
QMessageBox.warning(self, _("Error"), _("Failed to delete compatibility tool: {}").format(str(e)))
|
||||
|
||||
def open_winetricks(self):
|
||||
"""Open the Winetricks dialog for the selected prefix and wine."""
|
||||
selected_prefix = self.prefixCombo.currentText()
|
||||
if not selected_prefix:
|
||||
return
|
||||
|
||||
selected_wine = self.wineCombo.currentText()
|
||||
if not selected_wine:
|
||||
return
|
||||
|
||||
assert self.portproton_location is not None
|
||||
prefix_path = os.path.join(self.portproton_location, "data", "prefixes", selected_prefix)
|
||||
wine_path = os.path.join(self.portproton_location, "data", "dist", selected_wine, "bin", "wine")
|
||||
|
||||
# Open Winetricks dialog
|
||||
dialog = WinetricksDialog(self, self.theme, prefix_path, wine_path)
|
||||
dialog.exec()
|
||||
|
||||
def createPortProtonTab(self):
|
||||
"""Вкладка 'PortProton Settings'."""
|
||||
self.portProtonWidget = QWidget()
|
||||
|
@@ -916,6 +916,96 @@ SETTINGS_CHECKBOX_STYLE = f"""
|
||||
}}
|
||||
"""
|
||||
|
||||
WINETRICKS_TAB_STYLE = f"""
|
||||
QTabWidget::pane {{
|
||||
border: 1px solid {color_d};
|
||||
background: {color_b};
|
||||
border-radius: {border_radius_a};
|
||||
}}
|
||||
QTabBar::tab {{
|
||||
background: {color_c};
|
||||
color: {color_f};
|
||||
padding: 8px 16px;
|
||||
border-top-left-radius: {border_radius_a};
|
||||
border-top-right-radius: {border_radius_a};
|
||||
margin-right: 2px;
|
||||
}}
|
||||
QTabBar::tab:selected {{
|
||||
background: {color_a};
|
||||
color: {color_f};
|
||||
}}
|
||||
QTabBar::tab:hover {{
|
||||
background: {color_e};
|
||||
}}
|
||||
"""
|
||||
|
||||
WINETRICKS_TABBLE_STYLE = f"""
|
||||
QTableWidget {{
|
||||
background: {color_c};
|
||||
color: {color_f};
|
||||
gridline-color: {color_d};
|
||||
alternate-background-color: {color_d};
|
||||
border: {border_a};
|
||||
border-radius: {border_radius_a};
|
||||
font-family: '{font_family}';
|
||||
font-size: {font_size_a};
|
||||
}}
|
||||
QHeaderView::section {{
|
||||
background: {color_d};
|
||||
color: {color_f};
|
||||
padding: 5px;
|
||||
border: {border_a};
|
||||
font-weight: bold;
|
||||
}}
|
||||
QTableWidget::item {{
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid {color_d};
|
||||
}}
|
||||
QTableWidget::item:selected {{
|
||||
background: {color_a};
|
||||
color: {color_f};
|
||||
}}
|
||||
QTableWidget::item:hover {{
|
||||
background: {color_e};
|
||||
}}
|
||||
QTableWidget::indicator {{
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: {border_b} {color_a};
|
||||
border-radius: {border_radius_a};
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}}
|
||||
QTableWidget::indicator:unchecked {{
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
image: none;
|
||||
}}
|
||||
QTableWidget::indicator:checked {{
|
||||
background: {color_a};
|
||||
image: url({theme_manager.get_icon("check", current_theme_name, as_path=True)});
|
||||
border: {border_b} {color_f};
|
||||
}}
|
||||
QTableWidget::indicator:hover {{
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: {border_b} {color_a};
|
||||
}}
|
||||
QTableWidget::indicator:focus {{
|
||||
border: {border_c} {color_a};
|
||||
}}
|
||||
{SCROLL_AREA_STYLE}
|
||||
"""
|
||||
|
||||
WINETRICKS_LOG_STYLE = f"""
|
||||
QTextEdit {{
|
||||
background: {color_c};
|
||||
border: {border_a};
|
||||
border-radius: {border_radius_a};
|
||||
color: {color_f};
|
||||
font-family: '{font_family}';
|
||||
font-size: {font_size_a};
|
||||
padding: 5px;
|
||||
}}
|
||||
"""
|
||||
|
||||
FILE_EXPLORER_STYLE = f"""
|
||||
QListView {{
|
||||
font-size: {font_size_a};
|
||||
|
Reference in New Issue
Block a user