diff --git a/rare/components/tabs/downloads/widgets.py b/rare/components/tabs/downloads/widgets.py index 7f35e0f221..00f67f0987 100644 --- a/rare/components/tabs/downloads/widgets.py +++ b/rare/components/tabs/downloads/widgets.py @@ -96,7 +96,13 @@ def __init__(self, imgmgr: ImageManager, game: Game, igame: InstalledGame, paren def update_game(self, auto: bool): self.ui.update_button.setDisabled(True) self.ui.settings_button.setDisabled(True) - self.enqueue.emit(InstallOptionsModel(app_name=self.game.app_name, update=True, silent=auto)) # True if settings + self.enqueue.emit(InstallOptionsModel( + app_name=self.game.app_name, + base_path=self.igame.install_path, + platform=self.igame.platform, + update=True, + silent=auto, # True if settings + )) def set_enabled(self, enabled: bool): self.ui.update_button.setEnabled(enabled) diff --git a/rare/components/tabs/integrations/import_group.py b/rare/components/tabs/integrations/import_group.py index 46722ae291..29a7c662ab 100644 --- a/rare/components/tabs/integrations/import_group.py +++ b/rare/components/tabs/integrations/import_group.py @@ -102,16 +102,16 @@ def run(self) -> None: for i, child in enumerate(folders): if not child.is_dir(): continue - result = self.__try_import(child, None) + result = self._try_import(child, None) result_list.append(result) self.signals.progress.emit(result, int(100 * i // number_of_folders)) else: - result = self.__try_import(self.path, self.app_name) + result = self._try_import(self.path, self.app_name) result_list.append(result) self.signals.progress.emit(result, 100) self.signals.result.emit(result_list) - def __try_import(self, path: Path, app_name: str = None) -> ImportedGame: + def _try_import(self, path: Path, app_name: str = None) -> ImportedGame: result = ImportedGame(ImportResult.ERROR) result.path = str(path) if app_name or (app_name := find_app_name(self.core, str(path))): @@ -121,7 +121,7 @@ def __try_import(self, path: Path, app_name: str = None) -> ImportedGame: platform = self.platform if platform not in self.core.get_game(app_name, update_meta=False).asset_infos: platform = "Windows" - success, message = self.__import_game(path, app_name, platform) + success, message = self._import_game(path, app_name, platform) if not success: result.result = ImportResult.FAILED result.message = message @@ -129,7 +129,7 @@ def __try_import(self, path: Path, app_name: str = None) -> ImportedGame: result.result = ImportResult.SUCCESS return result - def __import_game(self, path: Path, app_name: str, platform: str): + def _import_game(self, path: Path, app_name: str, platform: str): cli = LegendaryCLI(self.core) status = LgndrIndirectStatus() args = LgndrImportGameArgs( @@ -158,17 +158,17 @@ def __init__(self, rcore: RareCore, parent=None): self.worker: Optional[ImportWorker] = None self.threadpool = QThreadPool.globalInstance() - self.__app_names: Dict[str, str] = None - self.__app_titles: Dict[str, str] = None - self.__install_dirs: Set[str] = None + self._app_names: Dict[str, str] = {} + self._app_titles: Dict[str, str] = {} + self._install_dirs: Set[str] = set() self.path_edit = PathEdit( path=self.core.get_default_install_dir(self.core.default_platform), file_mode=QFileDialog.FileMode.Directory, - edit_func=self.__path_edit_callback, + edit_func=self._path_edit_callback, parent=self, ) - self.path_edit.textChanged.connect(self.__path_changed) + self.path_edit.textChanged.connect(self._path_changed) self.ui.import_layout.setWidget( self.ui.import_layout.getWidgetPosition(self.ui.path_edit_label)[0], QFormLayout.ItemRole.FieldRole, @@ -177,11 +177,11 @@ def __init__(self, rcore: RareCore, parent=None): self.app_name_edit = IndicatorLineEdit( placeholder=self.tr("Use in case the app name was not found automatically"), - edit_func=self.__app_name_edit_callback, - save_func=self.__app_name_save_callback, + edit_func=self._app_name_edit_callback, + save_func=self._app_name_save_callback, parent=self, ) - self.app_name_edit.textChanged.connect(self.__app_name_changed) + self.app_name_edit.textChanged.connect(self._app_name_changed) self.ui.import_layout.setWidget( self.ui.import_layout.getWidgetPosition(self.ui.app_name_label)[0], QFormLayout.ItemRole.FieldRole, @@ -194,7 +194,7 @@ def __init__(self, rcore: RareCore, parent=None): self.ui.import_button_label.setText("") self.ui.import_button.setEnabled(False) - self.ui.import_button.clicked.connect(lambda: self.__import(self.path_edit.text())) + self.ui.import_button.clicked.connect(lambda: self._import(self.path_edit.text())) self.button_info_stack = QStackedWidget(self) self.button_info_stack.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) @@ -210,11 +210,14 @@ def __init__(self, rcore: RareCore, parent=None): def showEvent(self, a0: QShowEvent) -> None: if a0.spontaneous(): return super().showEvent(a0) - self.__app_names = {rgame.app_title: rgame.app_name for rgame in self.rcore.games} - self.__app_titles = {rgame.app_name: rgame.app_title for rgame in self.rcore.games} - self.__install_dirs = {rgame.folder_name for rgame in self.rcore.games if not rgame.is_dlc} - self.app_name_edit.setCompleter(ColumnCompleter(items=self.__app_names.items())) - super().showEvent(a0) + if not self._app_names: + self._app_names = {rgame.app_title: rgame.app_name for rgame in self.rcore.games} + if not self._app_titles: + self._app_titles = {rgame.app_name: rgame.app_title for rgame in self.rcore.games} + if not self._install_dirs: + self._install_dirs = {rgame.folder_name for rgame in self.rcore.games if not rgame.is_dlc} + self.app_name_edit.setCompleter(ColumnCompleter(items=self._app_names)) + return super().showEvent(a0) def set_game(self, app_name: str): if app_name: @@ -227,17 +230,17 @@ def set_game(self, app_name: str): ) self.app_name_edit.setText(app_name) - def __path_edit_callback(self, path) -> Tuple[bool, str, int]: + def _path_edit_callback(self, path) -> Tuple[bool, str, int]: if not os.path.exists(path): return False, path, IndicatorReasonsCommon.DIR_NOT_EXISTS if os.path.exists(os.path.join(path, ".egstore")): return True, path, IndicatorReasonsCommon.VALID - elif os.path.basename(path) in self.__install_dirs: + elif os.path.basename(path) in self._install_dirs: return True, path, IndicatorReasonsCommon.VALID return False, path, IndicatorReasonsCommon.INVALID @Slot(str) - def __path_changed(self, path: str): + def _path_changed(self, path: str): self.info_label.setText("") self.ui.import_folder_check.setCheckState(Qt.CheckState.Unchecked) self.ui.import_force_check.setCheckState(Qt.CheckState.Unchecked) @@ -246,18 +249,18 @@ def __path_changed(self, path: str): else: self.app_name_edit.setText("") - def __app_name_edit_callback(self, text) -> Tuple[bool, str, int]: + def _app_name_edit_callback(self, text) -> Tuple[bool, str, int]: self.app_name_edit.setInfo("") if not text: return False, text, IndicatorReasonsCommon.UNDEFINED - if text in self.__app_names.keys(): - return True, self.__app_names[text], IndicatorReasonsCommon.VALID - if text in self.__app_titles.keys(): + if text in self._app_names.keys(): + return True, self._app_names[text], IndicatorReasonsCommon.VALID + if text in self._app_titles.keys(): return True, text, IndicatorReasonsCommon.VALID else: return False, text, IndicatorReasonsCommon.GAME_NOT_EXISTS - def __app_name_save_callback(self, text) -> None: + def _app_name_save_callback(self, text) -> None: rgame = self.rcore.get_game(text) self.app_name_edit.setInfo(rgame.app_title) self.ui.platform_combo.clear() @@ -265,7 +268,7 @@ def __app_name_save_callback(self, text) -> None: self.ui.platform_combo.setCurrentText(rgame.default_platform) @Slot(str) - def __app_name_changed(self, app_name: str): + def _app_name_changed(self, app_name: str): self.info_label.setText("") self.ui.import_dlcs_check.setCheckState(Qt.CheckState.Unchecked) self.ui.import_force_check.setCheckState(Qt.CheckState.Unchecked) @@ -303,7 +306,7 @@ def import_dlcs_changed(self, state: Qt.CheckState): ) @Slot(str) - def __import(self, path: Optional[str] = None): + def _import(self, path: Optional[str] = None): self.ui.import_button.setDisabled(True) self.info_label.setText(self.tr("Status: Importing games")) self.info_progress.setValue(0) @@ -320,12 +323,12 @@ def __import(self, path: Optional[str] = None): import_dlcs=self.ui.import_dlcs_check.isChecked(), import_force=self.ui.import_force_check.isChecked(), ) - self.worker.signals.progress.connect(self.__on_import_progress) - self.worker.signals.result.connect(self.__on_import_result) + self.worker.signals.progress.connect(self._on_import_progress) + self.worker.signals.result.connect(self._on_import_result) self.threadpool.start(self.worker) @Slot(ImportedGame, int) - def __on_import_progress(self, imported: ImportedGame, progress: int): + def _on_import_progress(self, imported: ImportedGame, progress: int): self.info_progress.setValue(progress) if imported.result == ImportResult.SUCCESS: self.rcore.get_game(imported.app_name).set_installed(True) @@ -333,7 +336,7 @@ def __on_import_progress(self, imported: ImportedGame, progress: int): logger.info(f"Import {status}: {imported.app_title}: {imported.path} ({imported.message})") @Slot(list) - def __on_import_result(self, result: List[ImportedGame]): + def _on_import_result(self, result: List[ImportedGame]): self.worker = None self.button_info_stack.setCurrentWidget(self.info_label) if len(result) == 1: diff --git a/rare/components/tabs/library/details/compat.py b/rare/components/tabs/library/details/compat.py index dcbfe6ce9c..b406cb20ad 100644 --- a/rare/components/tabs/library/details/compat.py +++ b/rare/components/tabs/library/details/compat.py @@ -3,7 +3,7 @@ from typing import Tuple from PySide6.QtCore import QSignalBlocker, Qt, Slot -from PySide6.QtGui import QShowEvent +from PySide6.QtGui import QHideEvent, QShowEvent from rare.components.tabs.settings.compat import CompatSettingsBase from rare.components.tabs.settings.widgets.overlay import ( @@ -17,8 +17,8 @@ from rare.models.settings import RareAppSettings, app_settings from rare.shared import RareCore from rare.utils import config_helper as config -from rare.utils import steam_grades from rare.utils.paths import compat_shaders_dir, proton_compat_dir, wine_prefix_dir +from rare.utils.steam_grades import SteamGrades from rare.widgets.indicator_edit import ( ColumnCompleter, IndicatorLineEdit, @@ -74,36 +74,44 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): save_func=self.__steam_appid_save_callback, parent=self, ) - self.__steam_appids, self.__steam_titles = steam_grades.load_steam_appids() - self.steam_appid_edit.setCompleter(ColumnCompleter(items=self.__steam_appids.items())) self.form_layout.addRow(self.tr("Steam AppID"), self.steam_appid_edit) - def showEvent(self, a0: QShowEvent): - if a0.spontaneous(): - return super().showEvent(a0) + self.__grades = SteamGrades() + + def showEvent(self, e: QShowEvent): + if e.spontaneous(): + return super().showEvent(e) _ = QSignalBlocker(self.shader_cache_check) is_local_cache_enabled = self.settings.get_with_global(app_settings.local_shader_cache, self.rgame.app_name) has_local_cache_path = bool(config.get_envvar(self.app_name, "STEAM_COMPAT_SHADER_PATH", False)) self.shader_cache_check.setChecked(is_local_cache_enabled or has_local_cache_path) self.shader_cache_check.setChecked(is_local_cache_enabled or has_local_cache_path) _ = QSignalBlocker(self.steam_appid_edit) + items = {k: v for k, v in self.__grades.steam_appids.items() if self.rgame.app_title.lower()[0:4] in k.lower()[0:4]} + self.steam_appid_edit.setCompleter(ColumnCompleter(items=items)) self.steam_appid_edit.setText(self.rgame.steam_appid if self.rgame.steam_appid else "") - self.steam_appid_edit.setInfo(self.__steam_titles.get(self.rgame.steam_appid, "")) - return super().showEvent(a0) + self.steam_appid_edit.setInfo(self.__grades.steam_titles.get(self.rgame.steam_appid, "")) + return super().showEvent(e) + + def hideEvent(self, e: QHideEvent): + if e.spontaneous(): + return super().hideEvent(e) + self.steam_appid_edit.setCompleter(None) + return super().hideEvent(e) def __steam_appid_edit_callback(self, text: str) -> Tuple[bool, str, int]: self.steam_appid_edit.setInfo("") - if not text: + if not text or len(text) < 3: return True, text, IndicatorReasonsCommon.UNDEFINED - if text in self.__steam_appids.keys(): - return True, self.__steam_appids[text], IndicatorReasonsCommon.VALID - if text in self.__steam_titles.keys(): + if text in self.__grades.steam_appids.keys(): + return True, self.__grades.steam_appids[text], IndicatorReasonsCommon.VALID + if text in self.__grades.steam_titles.keys(): return True, text, IndicatorReasonsCommon.VALID else: return False, text, IndicatorReasonsCommon.GAME_NOT_EXISTS def __steam_appid_save_callback(self, text: str) -> None: - self.steam_appid_edit.setInfo(self.__steam_titles.get(text, "")) + self.steam_appid_edit.setInfo(self.__grades.steam_titles.get(text, "")) if text == self.rgame.steam_appid: return self.rgame.steam_appid = text diff --git a/rare/models/game.py b/rare/models/game.py index 267a2563d2..f2cbdf130c 100644 --- a/rare/models/game.py +++ b/rare/models/game.py @@ -20,7 +20,7 @@ from rare.shared.image_manager import ImageManager from rare.utils import config_helper as config from rare.utils.paths import compat_shaders_dir, data_dir, get_rare_executable -from rare.utils.steam_grades import get_rating +from rare.utils.steam_grades import SteamGrades from rare.utils.workarounds import apply_workarounds @@ -496,7 +496,7 @@ def get_steam_grade(self) -> str: return self.metadata.steam_grade def set_steam_grade(self) -> None: - appid, grade = get_rating(self.core, self.app_name, self.steam_appid) + appid, grade = SteamGrades().get_rating(self.core, self.app_name, self.steam_appid) if appid and self.steam_appid is None: self.steam_appid = appid self.metadata.steam_grade = grade diff --git a/rare/shared/workers/fetch.py b/rare/shared/workers/fetch.py index b293face98..4393cabad0 100644 --- a/rare/shared/workers/fetch.py +++ b/rare/shared/workers/fetch.py @@ -7,8 +7,8 @@ from rare.lgndr.core import LegendaryCore from rare.models.settings import RareAppSettings, app_settings -from rare.utils import steam_grades from rare.utils.metrics import timelogger +from rare.utils.steam_grades import SteamGrades from .worker import Worker @@ -38,8 +38,7 @@ def run_real(self): self.signals.progress.emit(0, self.signals.tr("Updating Steam AppIds")) with timelogger(self.logger, "Request Steam AppIds"): try: - with timelogger(self.logger, "steam grades load: "): - steam_grades.load_steam_appids() + SteamGrades().load_steam_appids() except Exception as e: self.logger.warning(e) self.signals.result.emit((), FetchWorker.Result.STEAMAPPIDS) diff --git a/rare/utils/steam_grades.py b/rare/utils/steam_grades.py index 6eed0b8664..7a0dd18a76 100644 --- a/rare/utils/steam_grades.py +++ b/rare/utils/steam_grades.py @@ -14,16 +14,6 @@ logger = getLogger("SteamGrades") -replace_chars = ",;.:-_ " -steamids_url = "https://raredevs.github.io/wring/steam_appids.json.xz" -protondb_url = "https://www.protondb.com/api/v1/reports/summaries/" - -__steam_appids: Dict = None -__steam_titles: Dict = None -__steam_appids_version: int = 3 -__active_download: bool = False - - class ProtondbRatings(int, Enum): # internal PENDING = ("pending", -2) @@ -49,95 +39,104 @@ def __int__(self): return self._value_ -def get_rating(core: LegendaryCore, app_name: str, steam_appid: int = None) -> Tuple[int, str]: - game = core.get_game(app_name) - try: - if steam_appid is None: - steam_appid = get_steam_id(game.app_title) - if not steam_appid: - raise RuntimeError - grade = get_grade(steam_appid) - except Exception as e: - logger.debug(e) - logger.error("Failed to get ProtonDB rating for %s", game.app_title) - return 0, "fail" - else: - return steam_appid, grade - - -# you should initiate the module with the game's steam code -def get_grade(steam_code): - if steam_code == 0: - return "fail" - steam_code = str(steam_code) - res = requests.get(f"{protondb_url}{steam_code}.json") - try: - app = orjson.loads(res.text) - except orjson.JSONDecodeError as e: - logger.debug(e) - logger.error("Failed to get ProtonDB response for %s", steam_code) - return "fail" - - return app.get("tier", "fail") - - -def download_steam_appids() -> bytes: - global __active_download - if __active_download: - return b"" - __active_download = True - resp = requests.get(steamids_url) - __active_download = False - return resp.content - - -def load_steam_appids() -> Tuple[Dict, Dict]: - global __steam_appids, __steam_titles - - if __steam_appids and __steam_titles: - return __steam_appids, __steam_titles - - file = os.path.join(cache_dir(), "steam_appids.json") - version = __steam_appids_version - elapsed_days = 0 - - if os.path.exists(file): - mod_time = datetime.fromtimestamp(os.path.getmtime(file)) - elapsed_days = abs(datetime.now() - mod_time).days - json = orjson.loads(open(file, "r").read()) - version = json.get("version", 0) - if version >= __steam_appids_version: - __steam_appids = json["games"] - - if not os.path.exists(file) or elapsed_days > 3 or version < __steam_appids_version: - if content := download_steam_appids(): - text = lzma.decompress(content).decode("utf-8") - with open(file, "w", encoding="utf-8") as f: - f.write(text) - json = orjson.loads(text) - __steam_appids = json["games"] - - __steam_titles = {v: k for k, v in __steam_appids.items()} - - return __steam_appids, __steam_titles - - -def get_steam_id(title: str) -> int: - # workarounds for satisfactory - # FIXME: This has to be made smarter. - title = title.replace("Early Access", "").replace("Experimental", "").strip() - # title = title.split(":")[0] - # title = title.split("-")[0] - global __steam_appids, __steam_titles - if not __steam_appids or not __steam_titles: - __steam_appids, __steam_titles = load_steam_appids() - - if title in __steam_titles.keys(): - steam_name = [title] - else: - steam_name = difflib.get_close_matches(title, __steam_appids.keys(), n=1, cutoff=0.5) - - if steam_name: - return __steam_appids[steam_name[0]] - else: - return 0 +class SteamGrades: + __steam_appids: Dict[str, str] = {} + __steam_appids_version: int = 3 + __active_download: bool = False + __replace_chars = ",;.:-_ " + __steamids_url = "https://raredevs.github.io/wring/steam_appids.json.xz" + __protondb_url = "https://www.protondb.com/api/v1/reports/summaries/" + + def __init__(self): + pass + + def _download_steam_appids(self) -> bytes: + if SteamGrades.__active_download: + return b"" + SteamGrades.__active_download = True + resp = requests.get(self.__steamids_url) + SteamGrades.__active_download = False + return resp.content + + def load_steam_appids(self) -> Dict: + + if SteamGrades.__steam_appids: + return SteamGrades.__steam_appids + + file = os.path.join(cache_dir(), "steam_appids.json") + version = SteamGrades.__steam_appids_version + elapsed_days = 0 + + if os.path.exists(file): + mod_time = datetime.fromtimestamp(os.path.getmtime(file)) + elapsed_days = abs(datetime.now() - mod_time).days + json = orjson.loads(open(file, "r").read()) + version = json.get("version", 0) + if version >= SteamGrades.__steam_appids_version: + SteamGrades.__steam_appids = json["games"] + + if not os.path.exists(file) or elapsed_days > 3 or version < SteamGrades.__steam_appids_version: + if content := self._download_steam_appids(): + text = lzma.decompress(content).decode("utf-8") + with open(file, "w", encoding="utf-8") as f: + f.write(text) + json = orjson.loads(text) + SteamGrades.__steam_appids = json["games"] + + return SteamGrades.__steam_appids + + @property + def steam_appids(self) -> Dict[str, str]: + if not SteamGrades.__steam_appids: + SteamGrades.__steam_appids = self.load_steam_appids() + return SteamGrades.__steam_appids + + @property + def steam_titles(self) -> Dict: + return {v: k for k, v in self.steam_appids.items()} + + def _get_steam_appid(self, title: str) -> str: + # workarounds for satisfactory + # FIXME: This has to be made smarter. + title = title.replace("Early Access", "").replace("Experimental", "").strip() + # title = title.split(":")[0] + # title = title.split("-")[0] + + if title in self.steam_titles.keys(): + steam_name = [title] + else: + steam_name = difflib.get_close_matches(title, self.steam_appids.keys(), n=1, cutoff=0.5) + + if steam_name: + return self.steam_appids[steam_name[0]] + else: + return "0" + + def _get_grade(self, steam_appid: str): + if steam_appid == "0": + return "fail" + steam_appid = str(steam_appid) + res = requests.get(f"{self.__protondb_url}/{steam_appid}.json") + try: + app = orjson.loads(res.text) + except orjson.JSONDecodeError as e: + logger.error(repr(e)) + logger.error("Failed to get ProtonDB response for %s", steam_appid) + return "fail" + + return app.get("tier", "fail") + + def get_rating(self, core: LegendaryCore, app_name: str, steam_appid: str = None) -> Tuple[str, str]: + game = core.get_game(app_name) + try: + if steam_appid is None: + steam_appid = self._get_steam_appid(game.app_title) + if not steam_appid: + raise RuntimeError + grade = self._get_grade(steam_appid) + except Exception as e: + logger.error(repr(e)) + logger.error("Failed to get ProtonDB rating for %s", game.app_title) + return "0", "fail" + else: + return steam_appid, grade diff --git a/rare/widgets/indicator_edit.py b/rare/widgets/indicator_edit.py index e5eb998fae..09a5525b63 100644 --- a/rare/widgets/indicator_edit.py +++ b/rare/widgets/indicator_edit.py @@ -1,5 +1,4 @@ import os -from collections.abc import ItemsView from enum import Enum, IntEnum from logging import getLogger from typing import Callable, Dict, Optional, Tuple @@ -221,6 +220,9 @@ def setCompleter(self, completer: Optional[QCompleter]): completer.popup().setAlternatingRowColors(True) self.line_edit.setCompleter(completer) + def completer(self) -> QCompleter: + return self.line_edit.completer() + @property def reasons(self): return self.__reasons @@ -405,23 +407,29 @@ def __set_path(self): class ColumnCompleter(QCompleter): - def __init__(self, items: ItemsView[str, str], parent=None): + def __init__(self, items: Optional[Dict[str, str]], parent=None): super(ColumnCompleter, self).__init__(parent) - model = QStandardItemModel(len(items), 2, self) - for idx, item in enumerate(items): - app_title, app_name = item - model.setData(model.index(idx, 0), app_title) - model.setData(model.index(idx, 1), app_name) - self.setModel(model) + self._treeview = QTreeView() + self.setPopup(self._treeview) + self._treeview.setRootIsDecorated(False) + self._treeview.header().hide() + # self._treeview.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) + # self._treeview.header().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) - treeview = QTreeView() - self.setPopup(treeview) - treeview.setRootIsDecorated(False) - treeview.header().hide() - treeview.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) - treeview.header().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) + if items: + self.setModel(items) self.setFilterMode(Qt.MatchFlag.MatchContains) self.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive) # self.setCompletionMode(QCompleter.UnfilteredPopupCompletion) + + def setModel(self, items: Dict[str, str]): + model = QStandardItemModel(len(items), 2, self) + for idx, item in enumerate(items.items()): + app_title, app_name = item + model.setData(model.index(idx, 0), app_title) + model.setData(model.index(idx, 1), app_name) + super().setModel(model) + self._treeview.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) + self._treeview.header().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)