diff --git a/portprotonqt/dialogs.py b/portprotonqt/dialogs.py index bb3e96b..ca3edc9 100644 --- a/portprotonqt/dialogs.py +++ b/portprotonqt/dialogs.py @@ -138,7 +138,7 @@ def create_dialog_hints_widget(theme, main_window, input_manager, context='defau elif context == 'proton_manager': dialog_actions = [ ("confirm", _("Toggle")), # A / Cross - ("add_game", _("Download")), # X / Triangle + ("add_game", _("Apply")), # X / Triangle ("prev_dir", _("Clear All")), # Y / Square ("back", _("Cancel")), # B / Circle ("prev_tab", _("Prev Tab")), # LB / L1 diff --git a/portprotonqt/get_wine_module.py b/portprotonqt/get_wine_module.py index cb4c809..2cc6ee6 100644 --- a/portprotonqt/get_wine_module.py +++ b/portprotonqt/get_wine_module.py @@ -355,13 +355,14 @@ class ProtonManager(QDialog): self.initUI() self.load_proton_data_from_json() + self.create_installed_tab() # Enable gamepad support if input manager is provided if self.input_manager: self.enable_proton_manager_mode() def initUI(self): - self.setWindowTitle(_('Get other Wine')) + self.setWindowTitle(_('Manage Wine versions')) self.resize(1100, 720) self.setStyleSheet(self.theme.MAIN_WINDOW_STYLE + self.theme.MESSAGE_BOX_STYLE) @@ -441,6 +442,9 @@ class ProtonManager(QDialog): button_layout.addWidget(self.clear_btn) layout.addLayout(button_layout) + # Connect tab change signal + self.tab_widget.currentChanged.connect(self.tab_changed) + # Connect signals for hints updates if self.input_manager and self.main_window: self.input_manager.button_event.connect( @@ -764,6 +768,132 @@ class ProtonManager(QDialog): return os.path.exists(expected_dir) + def create_installed_tab(self): + """Create the 'Installed' tab showing installed wine versions with removal option""" + if not self.portproton_location: + return + + dist_path = os.path.join(self.portproton_location, "data", "dist") + if not os.path.exists(dist_path): + os.makedirs(dist_path, exist_ok=True) + + installed_versions = [d for d in os.listdir(dist_path) if os.path.isdir(os.path.join(dist_path, d))] + + if not installed_versions: + # Create empty tab with message + tab = QWidget() + layout = QVBoxLayout(tab) + + label = QLabel(_("No Wine/Proton versions installed")) + label.setAlignment(Qt.AlignmentFlag.AlignCenter) + label.setStyleSheet("font-size: 16px; padding: 50px;") + layout.addWidget(label) + + self.tab_widget.addTab(tab, _("Installed")) + return + + # Create tab with table for installed versions + tab = QWidget() + tab.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + + layout = QVBoxLayout(tab) + layout.setContentsMargins(5, 5, 5, 5) + layout.setSpacing(5) + + table = QTableWidget() + table.setAlternatingRowColors(True) + table.verticalHeader().setVisible(False) + table.setColumnCount(3) # Checkbox, Name, Size + table.setHorizontalHeaderLabels(['', _('Version Name'), _('Size')]) + table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) + table.verticalHeader().setDefaultSectionSize(36) + table.setStyleSheet(self.theme.GETWINE_WINDOW_STYLE) + + header = table.horizontalHeader() + header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) + header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents) + + # Sort installed versions + installed_versions.sort(key=version_sort_key) + table.setRowCount(len(installed_versions)) + table.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + + for row_index, version_name in enumerate(installed_versions): + self.add_installed_row(table, row_index, version_name) + + layout.addWidget(table, 1) + + # Настройка выделения строк и обработчика кликов для вкладки Installed + table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) + table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection) + table.cellClicked.connect(self.on_cell_clicked) + + self.tab_widget.addTab(tab, _("Installed")) + + def add_installed_row(self, table, row_index, version_name): + """Add a row for an installed version with delete option""" + checkbox_widget = QWidget() + checkbox_layout = QHBoxLayout(checkbox_widget) + checkbox_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) + checkbox_layout.setContentsMargins(0, 0, 0, 0) + + checkbox = QCheckBox() + checkbox_widget.setToolTip(_("Select to remove this version")) + checkbox.stateChanged.connect(lambda state: self.on_installed_version_toggled(state)) + checkbox_layout.addWidget(checkbox) + + table.setCellWidget(row_index, 0, checkbox_widget) + + # Add version name + version_item = QTableWidgetItem(version_name) + version_item.setFlags(version_item.flags() & ~Qt.ItemFlag.ItemIsEditable) + table.setItem(row_index, 1, version_item) + + # Calculate and add size + if self.portproton_location: + dist_path = os.path.join(self.portproton_location, "data", "dist") + version_path = os.path.join(dist_path, version_name) + size_str = self.get_directory_size(version_path) + else: + size_str = _("Unknown") + version_path = "" # Provide a default value when portproton_location is None + size_item = QTableWidgetItem(size_str) + size_item.setFlags(size_item.flags() & ~Qt.ItemFlag.ItemIsEditable) + table.setItem(row_index, 2, size_item) + + # Store version name in user data for later use + for col in range(table.columnCount()): + item = table.item(row_index, col) + if item: + item.setData(Qt.ItemDataRole.UserRole, { + 'version_name': version_name, + 'version_path': version_path + }) + + def on_installed_version_toggled(self, state): + """Handle checkbox state changes in the installed tab""" + self.update_selection_display() + + def get_directory_size(self, path): + """Calculate directory size and return human-readable string""" + try: + total_size = 0 + for dirpath, _dirnames, filenames in os.walk(path): + for filename in filenames: + filepath = os.path.join(dirpath, filename) + if os.path.exists(filepath): + total_size += os.path.getsize(filepath) + + # Convert to human readable format + for unit in ['B', 'KB', 'MB', 'GB']: + if total_size < 1024.0: + return f"{total_size:.1f} {unit}" + total_size /= 1024.0 + return f"{total_size:.1f} TB" + except Exception: + return _("Unknown") + def on_cell_clicked(self, row): """Обработка клика по ячейке - переключение флажка при клике по любой ячейке в строке""" tab = self.tab_widget.currentWidget() @@ -774,6 +904,8 @@ class ProtonManager(QDialog): checkbox = checkbox_widget.findChild(QCheckBox) if checkbox and checkbox.isEnabled(): checkbox.setChecked(not checkbox.isChecked()) + # Update selection display after clicking + self.update_selection_display() def on_asset_toggled_json(self, state, asset, version, source_name): """Обработка выбора/отмены выбора элемента из данных JSON""" @@ -806,17 +938,102 @@ class ProtonManager(QDialog): def update_selection_display(self): """Обновляем отображение выбора""" - if self.selected_assets: - selection_text = _('Selected {} assets:\n').format(len(self.selected_assets)) + current_tab_index = self.tab_widget.currentIndex() + current_tab_text = self.tab_widget.tabText(current_tab_index) - for i, asset_data in enumerate(self.selected_assets.values(), 1): - selection_text += f"{i}. {asset_data['source_name'].upper()} - {asset_data['asset_name']}\n" + if current_tab_text == _("Installed"): + # Handle installed tab - count selected checkboxes + current_tab = self.tab_widget.currentWidget() + table = current_tab.findChild(QTableWidget) + if table: + selected_count = 0 + for row in range(table.rowCount()): + checkbox_widget = table.cellWidget(row, 0) + if checkbox_widget: + checkbox = checkbox_widget.findChild(QCheckBox) + if checkbox and checkbox.isChecked(): + selected_count += 1 - self.selection_text.setPlainText(selection_text) - self.download_btn.setEnabled(True) + if selected_count > 0: + selection_text = _('Selected {} assets:\n').format(selected_count) + + # Add the specific version names that are selected + current_tab = self.tab_widget.currentWidget() + table = current_tab.findChild(QTableWidget) + if table: + # Create a counter for numbering the selected items + item_number = 1 + for row in range(table.rowCount()): + checkbox_widget = table.cellWidget(row, 0) + if checkbox_widget: + checkbox = checkbox_widget.findChild(QCheckBox) + if checkbox and checkbox.isChecked(): + version_item = table.item(row, 1) # Version name column + if version_item: + version_name = version_item.text() + selection_text += f"{item_number}. {version_name}\n" + item_number += 1 + + self.download_btn.setText(_('Delete Selected')) + self.download_btn.setEnabled(True) + else: + selection_text = _("No assets selected") + self.download_btn.setText(_('Delete Selected')) + self.download_btn.setEnabled(False) + + self.selection_text.setPlainText(selection_text) + else: + self.selection_text.setPlainText(_("No assets selected")) + self.download_btn.setText(_('Delete Selected')) + self.download_btn.setEnabled(False) else: - self.selection_text.setPlainText(_("No assets selected")) - self.download_btn.setEnabled(False) + # Handle other tabs - use selected_assets dictionary + if self.selected_assets: + selection_text = _('Selected {} assets:\n').format(len(self.selected_assets)) + + for i, asset_data in enumerate(self.selected_assets.values(), 1): + selection_text += f"{i}. {asset_data['source_name'].upper()} - {asset_data['asset_name']}\n" + + self.selection_text.setPlainText(selection_text) + self.download_btn.setText(_('Download Selected')) + self.download_btn.setEnabled(True) + else: + self.selection_text.setPlainText(_("No assets selected")) + self.download_btn.setText(_('Download Selected')) + self.download_btn.setEnabled(False) + + def tab_changed(self, index): + """Handle tab change to update button text appropriately""" + current_tab_text = self.tab_widget.tabText(index) + if current_tab_text == _("Installed"): + # Count selected items in installed tab + current_tab = self.tab_widget.widget(index) + table = current_tab.findChild(QTableWidget) + if table: + selected_count = 0 + for row in range(table.rowCount()): + checkbox_widget = table.cellWidget(row, 0) + if checkbox_widget: + checkbox = checkbox_widget.findChild(QCheckBox) + if checkbox and checkbox.isChecked(): + selected_count += 1 + + if selected_count > 0: + self.download_btn.setText(_('Delete Selected')) + self.download_btn.setEnabled(True) + else: + self.download_btn.setText(_('Delete Selected')) + self.download_btn.setEnabled(False) + else: + # For other tabs, use the selected_assets dictionary + if self.selected_assets: + self.download_btn.setText(_('Download Selected')) + self.download_btn.setEnabled(True) + else: + self.download_btn.setText(_('Download Selected')) + self.download_btn.setEnabled(False) + + self.update_selection_display() def clear_selection(self): """Очищаем (сбрасываем) всё выбранное""" @@ -824,8 +1041,10 @@ class ProtonManager(QDialog): QMessageBox.warning(self, _("Downloading in Progress"), _("Cannot clear selection while extraction is in progress.")) return + # Clear selected assets for download tabs self.selected_assets.clear() + # Clear checkboxes in all tabs for tab_index in range(self.tab_widget.count()): tab = self.tab_widget.widget(tab_index) table = tab.findChild(QTableWidget) @@ -834,29 +1053,123 @@ class ProtonManager(QDialog): checkbox_widget = table.cellWidget(row, 0) if checkbox_widget: checkbox = checkbox_widget.findChild(QCheckBox) - if checkbox and checkbox.isEnabled(): + if checkbox: checkbox.setChecked(False) self.update_selection_display() def download_selected(self): - """Extract all selected archives""" - if not self.selected_assets: - QMessageBox.warning(self, _("No Selection"), _("Please select at least one archive to download.")) + """Handle both downloading new versions and removing installed versions""" + # Check if we're on the Installed tab + current_tab_index = self.tab_widget.currentIndex() + current_tab_text = self.tab_widget.tabText(current_tab_index) + + if current_tab_text == _("Installed"): + # Handle removal of selected installed versions + self.remove_selected_installed_versions() + else: + # Handle downloading of selected versions (existing functionality) + if not self.selected_assets: + QMessageBox.warning(self, _("No Selection"), _("Please select at least one archive to download.")) + return + + if self.is_downloading: + QMessageBox.warning(self, _("Downloading in Progress"), _("Please wait for current downloading to complete.")) + return + + downloads_dir = "proton_downloads" + if not os.path.exists(downloads_dir): + os.makedirs(downloads_dir) + + self.assets_to_download = list(self.selected_assets.values()) + self.current_download_index = 0 + self.is_downloading = True + self.start_next_download() + + def remove_selected_installed_versions(self): + """Delete selected installed wine/proton versions""" + # Get the current tab (Installed tab) + current_tab = self.tab_widget.currentWidget() + table = current_tab.findChild(QTableWidget) + if not table: return - if self.is_downloading: - QMessageBox.warning(self, _("Downloading in Progress"), _("Please wait for current downloading to complete.")) + # Find all selected versions to remove + versions_to_remove = [] + for row in range(table.rowCount()): + checkbox_widget = table.cellWidget(row, 0) + if checkbox_widget: + checkbox = checkbox_widget.findChild(QCheckBox) + if checkbox and checkbox.isChecked(): + item = table.item(row, 1) # Version name column + if item: + user_data = item.data(Qt.ItemDataRole.UserRole) + if user_data: + versions_to_remove.append(user_data['version_path']) + + if not versions_to_remove: + # Temporarily disable proton manager mode to allow gamepad input in QMessageBox + if self.input_manager: + self.disable_proton_manager_mode() + try: + QMessageBox.warning(self, _("No Selection"), _("Please select at least one version to delete.")) + finally: + # Re-enable proton manager mode after QMessageBox closes + if self.input_manager: + self.enable_proton_manager_mode() return - downloads_dir = "proton_downloads" - if not os.path.exists(downloads_dir): - os.makedirs(downloads_dir) + # Temporarily disable proton manager mode to allow gamepad input in QMessageBox + if self.input_manager: + self.disable_proton_manager_mode() + try: + # Confirm deletion + reply = QMessageBox.question( + self, + _("Confirm Deletion"), + _("Are you sure you want to delete {} selected version(s)?\n\nThis action cannot be undone.").format(len(versions_to_remove)), + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No + ) + finally: + # Re-enable proton manager mode after QMessageBox closes + if self.input_manager: + self.enable_proton_manager_mode() - self.assets_to_download = list(self.selected_assets.values()) - self.current_download_index = 0 - self.is_downloading = True - self.start_next_download() + if reply != QMessageBox.StandardButton.Yes: + return + + # Remove the selected versions + removed_count = 0 + for version_path in versions_to_remove: + try: + if os.path.exists(version_path): + import shutil + shutil.rmtree(version_path) + removed_count += 1 + except Exception as e: + logger.error(f"Error removing version at {version_path}: {e}") + QMessageBox.warning(self, _("Error"), _("Failed to remove version at {}: {}").format(version_path, str(e))) + + if removed_count > 0: + QMessageBox.information(self, _("Success"), _("Successfully removed {} version(s).").format(removed_count)) + # Refresh the installed tab to show updated list + self.refresh_installed_tab() + + def refresh_installed_tab(self): + """Refresh the installed tab to show current installed versions""" + # Find the installed tab index + installed_tab_index = -1 + for i in range(self.tab_widget.count()): + if self.tab_widget.tabText(i) == _("Installed"): + installed_tab_index = i + break + + if installed_tab_index != -1: + # Remove the old installed tab + self.tab_widget.removeTab(installed_tab_index) + # Create a new one + self.create_installed_tab() def start_next_download(self): """Start extraction of next archive in the list"""