ui: smarter override candidates picker
Add per-tab quick filters, hide-linkdown, and in-dialog status; live-mark already-added entries.
This commit is contained in:
@@ -686,6 +686,8 @@ RU: Скрывает элементы, которые уже есть в Force V
|
||||
self._tab_kind: dict[QWidget, str] = {}
|
||||
self._tab_list: dict[QWidget, QListWidget] = {}
|
||||
self._tab_filter: dict[QWidget, QLineEdit] = {}
|
||||
self._list_kind: dict[QListWidget, str] = {}
|
||||
self._list_title: dict[QListWidget, str] = {}
|
||||
self.tabs.currentChanged.connect(lambda _idx: self._refilter_current())
|
||||
|
||||
self._build_subnets_tab()
|
||||
@@ -707,6 +709,11 @@ RU: Скрывает элементы, которые уже есть в Force V
|
||||
row.addWidget(btn_close)
|
||||
root.addLayout(row)
|
||||
|
||||
self.lbl_status = QLabel("—")
|
||||
self.lbl_status.setWordWrap(True)
|
||||
self.lbl_status.setStyleSheet("color: gray;")
|
||||
root.addWidget(self.lbl_status)
|
||||
|
||||
def _mark_state(self, kind: str, value: str) -> tuple[bool, bool]:
|
||||
k = (kind or "").strip().lower()
|
||||
v = (value or "").strip()
|
||||
@@ -716,9 +723,32 @@ RU: Скрывает элементы, которые уже есть в Force V
|
||||
in_direct = v in (self.existing.get("direct", {}).get(k, set()) or set())
|
||||
return bool(in_vpn), bool(in_direct)
|
||||
|
||||
def _set_status(self, msg: str, ok: bool | None = None) -> None:
|
||||
text = (msg or "").strip() or "—"
|
||||
self.lbl_status.setText(text)
|
||||
if ok is True:
|
||||
self.lbl_status.setStyleSheet("color: green;")
|
||||
elif ok is False:
|
||||
self.lbl_status.setStyleSheet("color: red;")
|
||||
else:
|
||||
self.lbl_status.setStyleSheet("color: gray;")
|
||||
|
||||
def _apply_filter(self, lst: QListWidget, query: str) -> None:
|
||||
q = (query or "").strip().lower()
|
||||
hide_existing = bool(self.chk_hide_existing.isChecked())
|
||||
kind = (self._list_kind.get(lst) or "").strip().lower()
|
||||
|
||||
# Subnets-only quick filters.
|
||||
allow_lan = True
|
||||
allow_docker = True
|
||||
allow_link = True
|
||||
allow_linkdown = True
|
||||
if kind == "subnet":
|
||||
allow_lan = bool(getattr(self, "chk_sub_show_lan", None) and self.chk_sub_show_lan.isChecked())
|
||||
allow_docker = bool(getattr(self, "chk_sub_show_docker", None) and self.chk_sub_show_docker.isChecked())
|
||||
allow_link = bool(getattr(self, "chk_sub_show_link", None) and self.chk_sub_show_link.isChecked())
|
||||
allow_linkdown = not bool(getattr(self, "chk_sub_hide_linkdown", None) and self.chk_sub_hide_linkdown.isChecked())
|
||||
|
||||
for i in range(lst.count()):
|
||||
it = lst.item(i)
|
||||
if not it:
|
||||
@@ -726,6 +756,23 @@ RU: Скрывает элементы, которые уже есть в Force V
|
||||
if hide_existing and bool(it.data(QtCore.Qt.UserRole + 1) or False):
|
||||
it.setHidden(True)
|
||||
continue
|
||||
|
||||
if kind == "subnet":
|
||||
it_kind = str(it.data(QtCore.Qt.UserRole + 4) or "").strip().lower()
|
||||
it_linkdown = bool(it.data(QtCore.Qt.UserRole + 5) or False)
|
||||
if it_linkdown and not allow_linkdown:
|
||||
it.setHidden(True)
|
||||
continue
|
||||
if it_kind == "docker" and not allow_docker:
|
||||
it.setHidden(True)
|
||||
continue
|
||||
if it_kind == "lan" and not allow_lan:
|
||||
it.setHidden(True)
|
||||
continue
|
||||
if it_kind == "link" and not allow_link:
|
||||
it.setHidden(True)
|
||||
continue
|
||||
|
||||
if not q:
|
||||
it.setHidden(False)
|
||||
continue
|
||||
@@ -741,6 +788,42 @@ RU: Скрывает элементы, которые уже есть в Force V
|
||||
return
|
||||
self._apply_filter(lst, filt.text())
|
||||
|
||||
def _filter_for_title(self, title: str) -> QLineEdit | None:
|
||||
for i in range(self.tabs.count()):
|
||||
if self.tabs.tabText(i) == title:
|
||||
tab = self.tabs.widget(i)
|
||||
return self._tab_filter.get(tab)
|
||||
return None
|
||||
|
||||
def _preset_set_filter(self, title: str, text: str) -> None:
|
||||
filt = self._filter_for_title(title)
|
||||
if filt is not None:
|
||||
filt.setText(text)
|
||||
|
||||
def _update_item_render(self, it: QListWidgetItem, kind: str) -> None:
|
||||
value = str(it.data(QtCore.Qt.UserRole) or "").strip()
|
||||
base_label = str(it.data(QtCore.Qt.UserRole + 2) or it.text() or "")
|
||||
base_tip = str(it.data(QtCore.Qt.UserRole + 3) or it.toolTip() or "")
|
||||
in_vpn, in_direct = self._mark_state(kind, value)
|
||||
flags = []
|
||||
if in_vpn:
|
||||
flags.append("VPN")
|
||||
if in_direct:
|
||||
flags.append("DIRECT")
|
||||
label = base_label
|
||||
if flags:
|
||||
label = f"{base_label} [{' + '.join(flags)}]"
|
||||
it.setText(label)
|
||||
it.setData(QtCore.Qt.UserRole + 1, bool(in_vpn or in_direct))
|
||||
if base_tip.strip():
|
||||
extra_tip = (
|
||||
f"\n\nAlready in Force VPN: {'yes' if in_vpn else 'no'}\n"
|
||||
f"Already in Force Direct: {'yes' if in_direct else 'no'}"
|
||||
)
|
||||
it.setToolTip(base_tip + extra_tip)
|
||||
if in_vpn or in_direct:
|
||||
it.setForeground(QtGui.QBrush(QtGui.QColor("gray")))
|
||||
|
||||
def _add_tab(self, title: str, kind: str, items: list[tuple[str, str, str]], *, extra=None) -> None:
|
||||
tab = QWidget()
|
||||
layout = QVBoxLayout(tab)
|
||||
@@ -758,27 +841,16 @@ RU: Скрывает элементы, которые уже есть в Force V
|
||||
label = str(entry[0]) if len(entry) > 0 else ""
|
||||
value = str(entry[1]) if len(entry) > 1 else ""
|
||||
tip = str(entry[2]) if len(entry) > 2 else ""
|
||||
in_vpn, in_direct = self._mark_state(kind, value)
|
||||
flags = []
|
||||
if in_vpn:
|
||||
flags.append("VPN")
|
||||
if in_direct:
|
||||
flags.append("DIRECT")
|
||||
if flags:
|
||||
label = f"{label} [{' + '.join(flags)}]"
|
||||
|
||||
meta_kind = str(entry[3]) if len(entry) > 3 else ""
|
||||
meta_linkdown = bool(entry[4]) if len(entry) > 4 else False
|
||||
it = QListWidgetItem(label)
|
||||
it.setData(QtCore.Qt.UserRole, value)
|
||||
it.setData(QtCore.Qt.UserRole + 1, bool(in_vpn or in_direct))
|
||||
if tip.strip():
|
||||
extra_tip = (
|
||||
f"\n\nAlready in Force VPN: {'yes' if in_vpn else 'no'}\n"
|
||||
f"Already in Force Direct: {'yes' if in_direct else 'no'}"
|
||||
)
|
||||
it.setToolTip(tip + extra_tip)
|
||||
if in_vpn or in_direct:
|
||||
it.setForeground(QtGui.QBrush(QtGui.QColor("gray")))
|
||||
it.setData(QtCore.Qt.UserRole + 2, label) # base label (without [VPN]/[DIRECT])
|
||||
it.setData(QtCore.Qt.UserRole + 3, tip) # base tooltip (without existing-state)
|
||||
it.setData(QtCore.Qt.UserRole + 4, meta_kind)
|
||||
it.setData(QtCore.Qt.UserRole + 5, meta_linkdown)
|
||||
lst.addItem(it)
|
||||
self._update_item_render(it, kind)
|
||||
|
||||
layout.addWidget(lst, stretch=1)
|
||||
filt.textChanged.connect(lambda txt, l=lst: self._apply_filter(l, txt))
|
||||
@@ -787,6 +859,8 @@ RU: Скрывает элементы, которые уже есть в Force V
|
||||
self._tab_kind[tab] = kind
|
||||
self._tab_list[tab] = lst
|
||||
self._tab_filter[tab] = filt
|
||||
self._list_kind[lst] = kind
|
||||
self._list_title[lst] = title
|
||||
|
||||
def _current_kind_and_list(self) -> tuple[str, QListWidget | None]:
|
||||
tab = self.tabs.currentWidget()
|
||||
@@ -815,8 +889,52 @@ RU: Скрывает элементы, которые уже есть в Force V
|
||||
seen.add(v)
|
||||
out.append(v)
|
||||
|
||||
if out:
|
||||
self.add_cb(target, kind, out)
|
||||
if not out:
|
||||
self._set_status("Nothing selected", ok=None)
|
||||
return
|
||||
|
||||
tgt = (target or "").strip().lower()
|
||||
k = (kind or "").strip().lower()
|
||||
other = "direct" if tgt == "vpn" else "vpn"
|
||||
have_tgt = self.existing.get(tgt, {}).get(k, set()) or set()
|
||||
have_other = self.existing.get(other, {}).get(k, set()) or set()
|
||||
|
||||
to_add: list[str] = []
|
||||
skipped = 0
|
||||
conflicts = 0
|
||||
for v in out:
|
||||
if v in have_tgt:
|
||||
skipped += 1
|
||||
continue
|
||||
if v in have_other:
|
||||
conflicts += 1
|
||||
to_add.append(v)
|
||||
|
||||
if not to_add:
|
||||
self._set_status(f"Nothing new to add (skipped={skipped}, conflicts={conflicts})", ok=None)
|
||||
return
|
||||
|
||||
self.add_cb(target, kind, to_add)
|
||||
|
||||
# Update local state so UI marks newly added items immediately.
|
||||
if tgt not in self.existing:
|
||||
self.existing[tgt] = {}
|
||||
if k not in self.existing[tgt]:
|
||||
self.existing[tgt][k] = set()
|
||||
for v in to_add:
|
||||
self.existing[tgt][k].add(v)
|
||||
|
||||
for i in range(lst.count()):
|
||||
it = lst.item(i)
|
||||
if it is None:
|
||||
continue
|
||||
self._update_item_render(it, kind)
|
||||
self._refilter_current()
|
||||
|
||||
msg = f"Added {len(to_add)} item(s) to Force {tgt.upper()} ({k})."
|
||||
if skipped or conflicts:
|
||||
msg += f" skipped={skipped} conflicts={conflicts}"
|
||||
self._set_status(msg, ok=True)
|
||||
|
||||
def _list_for_title(self, title: str) -> QListWidget | None:
|
||||
for i in range(self.tabs.count()):
|
||||
@@ -866,7 +984,7 @@ RU: Скрывает элементы, которые уже есть в Force V
|
||||
def _build_subnets_tab(self) -> None:
|
||||
subs = list(getattr(self.cands, "subnets", []) or [])
|
||||
|
||||
items: list[tuple[str, str, str]] = []
|
||||
items: list[tuple[str, str, str, str, bool]] = []
|
||||
for s in subs:
|
||||
cidr = str(getattr(s, "cidr", "") or "").strip()
|
||||
if not cidr:
|
||||
@@ -888,7 +1006,7 @@ RU: Скрывает элементы, которые уже есть в Force V
|
||||
"EN: Source subnet overrides affect forwarded traffic (Docker).\n"
|
||||
"RU: Source subnet влияет на forwarded трафик (Docker)."
|
||||
)
|
||||
items.append((f"{cidr}{tag_txt}", cidr, tip))
|
||||
items.append((f"{cidr}{tag_txt}", cidr, tip, kind, linkdown))
|
||||
|
||||
def extra(layout: QVBoxLayout) -> None:
|
||||
row = QHBoxLayout()
|
||||
@@ -901,6 +1019,47 @@ RU: Скрывает элементы, которые уже есть в Force V
|
||||
row.addStretch(1)
|
||||
layout.addLayout(row)
|
||||
|
||||
row2 = QHBoxLayout()
|
||||
btn_f_lan = QPushButton("Filter LAN")
|
||||
btn_f_lan.clicked.connect(lambda: self._preset_set_filter("Subnets", "lan"))
|
||||
row2.addWidget(btn_f_lan)
|
||||
btn_f_docker = QPushButton("Filter Docker")
|
||||
btn_f_docker.clicked.connect(lambda: self._preset_set_filter("Subnets", "docker"))
|
||||
row2.addWidget(btn_f_docker)
|
||||
btn_f_clear = QPushButton("Clear filter")
|
||||
btn_f_clear.clicked.connect(lambda: self._preset_set_filter("Subnets", ""))
|
||||
row2.addWidget(btn_f_clear)
|
||||
row2.addStretch(1)
|
||||
layout.addLayout(row2)
|
||||
|
||||
row3 = QHBoxLayout()
|
||||
self.chk_sub_show_lan = QCheckBox("LAN")
|
||||
self.chk_sub_show_lan.setChecked(True)
|
||||
self.chk_sub_show_lan.setToolTip("EN: Show LAN subnets.\nRU: Показать LAN подсети.")
|
||||
self.chk_sub_show_lan.stateChanged.connect(lambda _s: self._refilter_current())
|
||||
row3.addWidget(self.chk_sub_show_lan)
|
||||
|
||||
self.chk_sub_show_docker = QCheckBox("Docker")
|
||||
self.chk_sub_show_docker.setChecked(True)
|
||||
self.chk_sub_show_docker.setToolTip("EN: Show Docker/container subnets.\nRU: Показать Docker/контейнерные подсети.")
|
||||
self.chk_sub_show_docker.stateChanged.connect(lambda _s: self._refilter_current())
|
||||
row3.addWidget(self.chk_sub_show_docker)
|
||||
|
||||
self.chk_sub_show_link = QCheckBox("Link")
|
||||
self.chk_sub_show_link.setChecked(True)
|
||||
self.chk_sub_show_link.setToolTip("EN: Show link-scope routes.\nRU: Показать маршруты scope link.")
|
||||
self.chk_sub_show_link.stateChanged.connect(lambda _s: self._refilter_current())
|
||||
row3.addWidget(self.chk_sub_show_link)
|
||||
|
||||
self.chk_sub_hide_linkdown = QCheckBox("Hide linkdown")
|
||||
self.chk_sub_hide_linkdown.setChecked(True)
|
||||
self.chk_sub_hide_linkdown.setToolTip("EN: Hide routes marked as linkdown.\nRU: Скрыть маршруты с меткой linkdown.")
|
||||
self.chk_sub_hide_linkdown.stateChanged.connect(lambda _s: self._refilter_current())
|
||||
row3.addWidget(self.chk_sub_hide_linkdown)
|
||||
|
||||
row3.addStretch(1)
|
||||
layout.addLayout(row3)
|
||||
|
||||
self._add_tab("Subnets", "subnet", items, extra=extra)
|
||||
|
||||
def _preset_add_lan_direct(self) -> None:
|
||||
@@ -964,6 +1123,19 @@ RU: Скрывает элементы, которые уже есть в Force V
|
||||
row.addStretch(1)
|
||||
layout.addLayout(row)
|
||||
|
||||
row2 = QHBoxLayout()
|
||||
btn_f_docker = QPushButton("Filter docker")
|
||||
btn_f_docker.clicked.connect(lambda: self._preset_set_filter("Services", "docker"))
|
||||
row2.addWidget(btn_f_docker)
|
||||
btn_f_media = QPushButton("Filter media")
|
||||
btn_f_media.clicked.connect(lambda: self._preset_set_filter("Services", "jellyfin"))
|
||||
row2.addWidget(btn_f_media)
|
||||
btn_f_clear = QPushButton("Clear filter")
|
||||
btn_f_clear.clicked.connect(lambda: self._preset_set_filter("Services", ""))
|
||||
row2.addWidget(btn_f_clear)
|
||||
row2.addStretch(1)
|
||||
layout.addLayout(row2)
|
||||
|
||||
self._add_tab("Services", "cgroup", items, extra=extra)
|
||||
|
||||
def _build_uids_tab(self) -> None:
|
||||
|
||||
Reference in New Issue
Block a user