Files
diskmanager/dialogs.py
2026-02-03 04:49:37 +08:00

487 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# dialogs.py
import os
from PySide6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QLineEdit, QComboBox, QDoubleSpinBox,
QPushButton, QFormLayout, QFileDialog,
QMessageBox, QCheckBox, QListWidget, QListWidgetItem)
from PySide6.QtCore import Qt
from logger_config import setup_logging, logger
class CreatePartitionDialog(QDialog):
def __init__(self, parent=None, disk_path="", total_disk_mib=0.0, max_available_mib=0.0):
super().__init__(parent)
self.setWindowTitle("创建分区")
self.setMinimumWidth(300)
self.disk_path = disk_path
self.total_disk_mib = total_disk_mib # 磁盘总大小 (MiB)
self.max_available_mib = max_available_mib # 可用空间 (MiB)
logger.debug(f"CreatePartitionDialog initialized for {disk_path}. Total MiB: {total_disk_mib}, Max Available MiB: {max_available_mib}")
self.partition_table_type_combo = QComboBox()
self.partition_table_type_combo.addItems(["gpt", "msdos"])
self.size_spinbox = QDoubleSpinBox()
# 调整最小值为 0.01 GB (约 10MB),并确保最大值不小于最小值
calculated_max_gb = self.max_available_mib / 1024.0
self.size_spinbox.setMinimum(0.01)
self.size_spinbox.setMaximum(max(0.01, calculated_max_gb)) # 将 MiB 转换为 GB 显示
logger.debug(f"Size spinbox max set to: {self.size_spinbox.maximum()} GB (calculated from {calculated_max_gb} GB)")
self.size_spinbox.setSuffix(" GB")
self.size_spinbox.setDecimals(2)
self.use_max_space_checkbox = QCheckBox("使用最大可用空间")
self.use_max_space_checkbox.setChecked(True) # 默认选中
self._setup_ui()
self._connect_signals()
self._initialize_state()
def _setup_ui(self):
layout = QVBoxLayout(self)
form_layout = QFormLayout()
form_layout.addRow("磁盘路径:", QLabel(self.disk_path))
form_layout.addRow("分区表类型:", self.partition_table_type_combo)
form_layout.addRow("分区大小:", self.size_spinbox)
form_layout.addRow("", self.use_max_space_checkbox) # 添加复选框
layout.addLayout(form_layout)
button_box = QHBoxLayout()
self.confirm_button = QPushButton("确定")
self.cancel_button = QPushButton("取消")
button_box.addWidget(self.confirm_button)
button_box.addWidget(self.cancel_button)
layout.addLayout(button_box)
def _connect_signals(self):
self.confirm_button.clicked.connect(self.accept)
self.cancel_button.clicked.connect(self.reject)
# 信号连接保持不变,但 _toggle_size_input 方法内部将直接查询复选框状态
self.use_max_space_checkbox.stateChanged.connect(self._toggle_size_input)
def _initialize_state(self):
# 根据默认选中状态设置 spinbox
self._toggle_size_input(self.use_max_space_checkbox.checkState())
def _toggle_size_input(self, state): # state 参数仍然接收,但不再直接用于判断
# 直接查询复选框的当前状态,而不是依赖信号传递的 state 参数
is_checked = self.use_max_space_checkbox.isChecked()
logger.debug(f"[_toggle_size_input] Called. Checkbox isChecked(): {is_checked}, signal state: {state}")
if is_checked:
logger.debug(f"[_toggle_size_input] Checkbox is CHECKED. Disabling spinbox and setting value to max.")
self.size_spinbox.setEnabled(False) # 禁用 spinbox
max_val = self.size_spinbox.maximum()
self.size_spinbox.setValue(max_val) # 设置为最大可用 GB
logger.debug(f"[_toggle_size_input] Spinbox value set to max: {max_val} GB.")
else: # is_checked is False
logger.debug(f"[_toggle_size_input] Checkbox is UNCHECKED. Enabling spinbox.")
self.size_spinbox.setEnabled(True) # 启用 spinbox
# 如果之前是最大值,取消勾选后,恢复到最小值,方便用户输入自定义值
if self.size_spinbox.value() == self.size_spinbox.maximum():
min_val = self.size_spinbox.minimum()
self.size_spinbox.setValue(min_val)
logger.debug(f"[_toggle_size_input] Spinbox was at max, reset to min: {min_val} GB.")
else:
logger.debug(f"[_toggle_size_input] Spinbox was not at max, keeping current value: {self.size_spinbox.value()} GB.")
def get_partition_info(self):
size_gb = self.size_spinbox.value()
use_max_space = self.use_max_space_checkbox.isChecked()
if not use_max_space and size_gb <= 0:
QMessageBox.warning(self, "输入错误", "分区大小必须大于0。")
return None
# 这里的检查应该使用 self.size_spinbox.maximum() 来判断,因为它是实际的最大值
if not use_max_space and size_gb > self.size_spinbox.maximum():
QMessageBox.warning(self, "输入错误", "分区大小不能超过最大可用空间。")
return None
return {
'disk_path': self.disk_path,
'partition_table_type': self.partition_table_type_combo.currentText(),
'size_gb': size_gb,
'total_disk_mib': self.total_disk_mib, # 传递磁盘总大小 (MiB)
'use_max_space': use_max_space # 返回此标志
}
class MountDialog(QDialog):
def __init__(self, parent=None, device_path=""):
super().__init__(parent)
self.setWindowTitle("挂载分区")
self.setMinimumWidth(300)
self.device_path = device_path
self.mount_point_input = QLineEdit()
self.add_to_fstab_checkbox = QCheckBox("开机自动挂载 (添加到 /etc/fstab)") # 新增
self._setup_ui()
self._connect_signals()
def _setup_ui(self):
layout = QVBoxLayout(self)
form_layout = QFormLayout()
form_layout.addRow("设备路径:", QLabel(self.device_path))
form_layout.addRow("挂载点:", self.mount_point_input)
form_layout.addRow("", self.add_to_fstab_checkbox) # 添加复选框
layout.addLayout(form_layout)
button_box = QHBoxLayout()
self.confirm_button = QPushButton("确定")
self.cancel_button = QPushButton("取消")
button_box.addWidget(self.confirm_button)
button_box.addWidget(self.cancel_button)
layout.addLayout(button_box)
# 默认挂载点,可以根据设备名生成
default_mount_point = f"/mnt/{os.path.basename(self.device_path)}"
self.mount_point_input.setText(default_mount_point)
def _connect_signals(self):
self.confirm_button.clicked.connect(self.accept)
self.cancel_button.clicked.connect(self.reject)
def get_mount_info(self):
mount_point = self.mount_point_input.text().strip()
add_to_fstab = self.add_to_fstab_checkbox.isChecked()
if not mount_point:
QMessageBox.warning(self, "输入错误", "挂载点不能为空。")
return None
if not mount_point.startswith('/'):
QMessageBox.warning(self, "输入错误", "挂载点必须是绝对路径 (以 '/' 开头)。")
return None
return {
'mount_point': mount_point,
'add_to_fstab': add_to_fstab
}
class CreateRaidDialog(QDialog):
def __init__(self, parent=None, available_devices=None):
super().__init__(parent)
self.setWindowTitle("创建 RAID 阵列")
self.setMinimumWidth(350)
self.available_devices = available_devices if available_devices is not None else []
self.selected_devices = []
self.raid_level_combo = QComboBox()
self.raid_level_combo.addItems(["raid0", "raid1", "raid5", "raid6", "raid10"])
self.chunk_size_spinbox = QDoubleSpinBox()
self.chunk_size_spinbox.setMinimum(4)
self.chunk_size_spinbox.setMaximum(1024)
self.chunk_size_spinbox.setSingleStep(4)
self.chunk_size_spinbox.setValue(512) # 默认值
self.chunk_size_spinbox.setSuffix(" KB")
self.device_list_widget = QListWidget()
for dev in self.available_devices:
item = QListWidgetItem(dev)
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
item.setCheckState(Qt.Unchecked)
self.device_list_widget.addItem(item)
self._setup_ui()
self._connect_signals()
def _setup_ui(self):
layout = QVBoxLayout(self)
form_layout = QFormLayout()
form_layout.addRow("RAID 级别:", self.raid_level_combo)
form_layout.addRow("Chunk 大小:", self.chunk_size_spinbox)
form_layout.addRow("选择设备:", self.device_list_widget)
layout.addLayout(form_layout)
button_box = QHBoxLayout()
self.confirm_button = QPushButton("确定")
self.cancel_button = QPushButton("取消")
button_box.addWidget(self.confirm_button)
button_box.addWidget(self.cancel_button)
layout.addLayout(button_box)
def _connect_signals(self):
self.confirm_button.clicked.connect(self.accept)
self.cancel_button.clicked.connect(self.reject)
def get_raid_info(self):
self.selected_devices = []
for i in range(self.device_list_widget.count()):
item = self.device_list_widget.item(i)
if item.checkState() == Qt.Checked:
self.selected_devices.append(item.text())
if not self.selected_devices:
QMessageBox.warning(self, "输入错误", "请选择至少一个设备来创建 RAID 阵列。")
return None
raid_level = self.raid_level_combo.currentText()
num_devices = len(self.selected_devices)
# RAID level specific checks for minimum devices
if raid_level == "raid0" and num_devices < 1: # RAID0 can be 1 device in some contexts, but usually >1
QMessageBox.warning(self, "输入错误", "RAID0 至少需要一个设备。")
return None
elif raid_level == "raid1" and num_devices < 2:
QMessageBox.warning(self, "输入错误", "RAID1 至少需要两个设备。")
return None
elif raid_level == "raid5" and num_devices < 3:
QMessageBox.warning(self, "输入错误", "RAID5 至少需要三个设备。")
return None
elif raid_level == "raid6" and num_devices < 4:
QMessageBox.warning(self, "输入错误", "RAID6 至少需要四个设备。")
return None
elif raid_level == "raid10" and num_devices < 2: # RAID10 needs at least 2 (for 1+0)
QMessageBox.warning(self, "输入错误", "RAID10 至少需要两个设备。")
return None
return {
'devices': self.selected_devices,
'level': self.raid_level_combo.currentText().replace('raid', ''), # 移除 'raid' 前缀
'chunk_size': int(self.chunk_size_spinbox.value()) # KB
}
class CreatePvDialog(QDialog):
def __init__(self, parent=None, available_partitions=None):
super().__init__(parent)
self.setWindowTitle("创建物理卷 (PV)")
self.setMinimumWidth(300)
self.available_partitions = available_partitions if available_partitions is not None else []
self.device_combo_box = QComboBox()
self.device_combo_box.addItems(self.available_partitions)
self._setup_ui()
self._connect_signals()
def _setup_ui(self):
layout = QVBoxLayout(self)
form_layout = QFormLayout()
form_layout.addRow("选择设备:", self.device_combo_box)
layout.addLayout(form_layout)
button_box = QHBoxLayout()
self.confirm_button = QPushButton("确定")
self.cancel_button = QPushButton("取消")
button_box.addWidget(self.confirm_button)
button_box.addWidget(self.cancel_button)
layout.addLayout(button_box)
def _connect_signals(self):
self.confirm_button.clicked.connect(self.accept)
self.cancel_button.clicked.connect(self.reject)
def get_pv_info(self):
device_path = self.device_combo_box.currentText()
if not device_path:
QMessageBox.warning(self, "输入错误", "请选择一个设备来创建物理卷。")
return None
return {'device_path': device_path}
class CreateVgDialog(QDialog):
def __init__(self, parent=None, available_pvs=None):
super().__init__(parent)
self.setWindowTitle("创建卷组 (VG)")
self.setMinimumWidth(300)
self.available_pvs = available_pvs if available_pvs is not None else []
self.selected_pvs = []
self.vg_name_input = QLineEdit()
self.pv_list_widget = QListWidget()
for pv in self.available_pvs:
item = QListWidgetItem(pv)
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
item.setCheckState(Qt.Unchecked)
self.pv_list_widget.addItem(item)
self._setup_ui()
self._connect_signals()
def _setup_ui(self):
layout = QVBoxLayout(self)
form_layout = QFormLayout()
form_layout.addRow("卷组名称:", self.vg_name_input)
form_layout.addRow("选择物理卷:", self.pv_list_widget)
layout.addLayout(form_layout)
button_box = QHBoxLayout()
self.confirm_button = QPushButton("确定")
self.cancel_button = QPushButton("取消")
button_box.addWidget(self.confirm_button)
button_box.addWidget(self.cancel_button)
layout.addLayout(button_box)
def _connect_signals(self):
self.confirm_button.clicked.connect(self.accept)
self.cancel_button.clicked.connect(self.reject)
def get_vg_info(self):
vg_name = self.vg_name_input.text().strip()
self.selected_pvs = []
for i in range(self.pv_list_widget.count()):
item = self.pv_list_widget.item(i)
if item.checkState() == Qt.Checked:
self.selected_pvs.append(item.text())
if not vg_name:
QMessageBox.warning(self, "输入错误", "卷组名称不能为空。")
return None
if not self.selected_pvs:
QMessageBox.warning(self, "输入错误", "请选择至少一个物理卷来创建卷组。")
return None
return {
'vg_name': vg_name,
'pvs': self.selected_pvs
}
class CreateLvDialog(QDialog):
def __init__(self, parent=None, available_vgs=None, vg_sizes=None):
super().__init__(parent)
self.setWindowTitle("创建逻辑卷")
self.setMinimumWidth(300)
self.available_vgs = available_vgs if available_vgs is not None else []
self.vg_sizes = vg_sizes if vg_sizes is not None else {} # 存储每个 VG 的可用大小 (GB)
logger.debug(f"CreateLvDialog initialized. Available VGs: {self.available_vgs}, VG Sizes: {self.vg_sizes}")
self.lv_name_input = QLineEdit()
self.vg_combo_box = QComboBox()
self.lv_size_spinbox = QDoubleSpinBox()
self.use_max_space_checkbox = QCheckBox("使用最大可用空间")
self._setup_ui()
self._connect_signals()
self._initialize_state()
def _setup_ui(self):
layout = QVBoxLayout(self)
form_layout = QFormLayout()
form_layout.addRow("逻辑卷名称:", self.lv_name_input)
form_layout.addRow("选择卷组:", self.vg_combo_box)
form_layout.addRow("逻辑卷大小 (GB):", self.lv_size_spinbox)
form_layout.addRow("", self.use_max_space_checkbox)
layout.addLayout(form_layout)
button_box = QHBoxLayout()
self.confirm_button = QPushButton("确定")
self.cancel_button = QPushButton("取消")
button_box.addWidget(self.confirm_button)
button_box.addWidget(self.cancel_button)
layout.addLayout(button_box)
self.lv_size_spinbox.setMinimum(0.01) # 逻辑卷最小大小,例如 0.01 GB
self.lv_size_spinbox.setSuffix(" GB")
self.lv_size_spinbox.setDecimals(2)
# 设置一个较大的初始最大值,它将在 _update_size_options 中被实际的 VG 大小覆盖
self.lv_size_spinbox.setMaximum(100000.0) # 初始设置一个足够大的最大值
def _connect_signals(self):
self.confirm_button.clicked.connect(self.accept)
self.cancel_button.clicked.connect(self.reject)
self.vg_combo_box.currentIndexChanged.connect(self._update_size_options)
# 信号连接到不带参数的槽函数
self.use_max_space_checkbox.stateChanged.connect(self._toggle_size_input)
def _initialize_state(self):
self.vg_combo_box.addItems(self.available_vgs)
# 默认选中“使用最大可用空间”,这将触发 _toggle_size_input
self.use_max_space_checkbox.setChecked(True)
# 首次调用以设置初始状态和根据复选框状态设置值
# 这一步是必要的,即使 setChecked(True) 也会触发一次 _toggle_size_input
# 但 _update_size_options 会在设置了正确的最大值后再次调用 _toggle_size_input确保最终状态正确。
self._update_size_options()
logger.debug("CreateLvDialog _initialize_state completed.")
def _update_size_options(self):
"""
根据选中的卷组更新逻辑卷大小的范围 (最小值和最大值)。
并根据当前“使用最大可用空间”复选框的状态调整 spinbox 的值和启用状态。
"""
selected_vg = self.vg_combo_box.currentText()
max_size_gb = self.vg_sizes.get(selected_vg, 0.0)
logger.debug(f"[_update_size_options] Selected VG: {selected_vg}, Max available GB: {max_size_gb}")
# 确保实际最大可用空间至少大于或等于 spinbox 的最小值
# 避免当可用空间非常小时spinbox 无法设置有效值
effective_max_size_gb = max(self.lv_size_spinbox.minimum(), max_size_gb)
self.lv_size_spinbox.setMaximum(effective_max_size_gb)
logger.debug(f"[_update_size_options] Spinbox max set to: {effective_max_size_gb} GB.")
# 在设置了新的最大值后,重新应用复选框的状态来更新值和启用状态
self._toggle_size_input() # 调用不带参数的槽函数
logger.debug("[_update_size_options] Completed.")
def _toggle_size_input(self): # 不再接收 'state' 参数
"""
根据“使用最大可用空间”复选框的状态切换大小输入框的启用/禁用状态和值。
"""
is_checked = self.use_max_space_checkbox.isChecked() # 直接查询复选框的当前状态
logger.debug(f"[_toggle_size_input] Called. Checkbox isChecked(): {is_checked}")
current_max_spinbox_value = self.lv_size_spinbox.maximum()
current_spinbox_value = self.lv_size_spinbox.value()
if is_checked:
logger.debug(f"[_toggle_size_input] Checkbox is CHECKED. Disabling spinbox and setting value to max.")
self.lv_size_spinbox.setDisabled(True) # 禁用 spinbox
self.lv_size_spinbox.setValue(current_max_spinbox_value) # 设置为最大可用 GB
logger.debug(f"[_toggle_size_input] Spinbox value set to max: {current_max_spinbox_value} GB.")
else: # is_checked is False
logger.debug(f"[_toggle_size_input] Checkbox is UNCHECKED. Enabling spinbox.")
self.lv_size_spinbox.setDisabled(False) # 启用 spinbox
# 如果之前是最大值(因为复选框被勾选),取消勾选后,恢复到最小值,方便用户输入自定义值
if current_spinbox_value == current_max_spinbox_value:
min_val = self.lv_size_spinbox.minimum()
self.lv_size_spinbox.setValue(min_val)
logger.debug(f"[_toggle_size_input] Spinbox was at max, reset to min: {min_val} GB.")
else:
logger.debug(f"[_toggle_size_input] Spinbox was not at max, keeping current value: {self.lv_size_spinbox.value()} GB.")
logger.debug("[_toggle_size_input] Completed.")
def get_lv_info(self):
lv_name = self.lv_name_input.text().strip()
vg_name = self.vg_combo_box.currentText()
size_gb = self.lv_size_spinbox.value()
use_max_space = self.use_max_space_checkbox.isChecked() # 获取复选框状态
if not lv_name:
QMessageBox.warning(self, "输入错误", "逻辑卷名称不能为空。")
return None
if not vg_name:
QMessageBox.warning(self, "输入错误", "请选择一个卷组。")
return None
# 如果没有使用最大空间,则检查输入大小的有效性
if not use_max_space:
if size_gb <= 0:
QMessageBox.warning(self, "输入错误", "逻辑卷大小必须大于0。")
return None
# 这里的检查应该使用 self.lv_size_spinbox.maximum() 来判断,因为它是实际的最大值
if size_gb > self.lv_size_spinbox.maximum():
QMessageBox.warning(self, "输入错误", "逻辑卷大小不能超过卷组的可用空间。")
return None
return {
'lv_name': lv_name,
'vg_name': vg_name,
'size_gb': size_gb,
'use_max_space': use_max_space # 返回此标志
}