feat: dynamic port validation

This commit is contained in:
aclist 2026-01-12 23:50:27 +09:00
parent b119569cf5
commit fdb6b342f7
6 changed files with 153 additions and 149 deletions

View File

@ -694,7 +694,8 @@ class Controller:
if len(fav) < 1:
return None, None
ip = str(self.query_config(Preferences.FAV_SRV))
return fav, ip
addr = ip.split(":")
return fav, f"{addr[0]}:{addr[2]}"
def get_dist_cache(self) -> dict[str, "Haversine"]:
return self.dist_cache

View File

@ -512,31 +512,55 @@ server_labels = ServerLabels(
class ConnectPanel:
connect: str
add: str
edit: str
favorite: str
add_con: str
placeholder: str
entry_tooltip: str
add_tooltip: str
connect_tooltip: str
no_fav: str
connect_panel = ConnectPanel(
connect="Connect",
add="Add",
edit="Edit",
favorite="Favorite server",
add_con="Add/connect",
placeholder="Enter IP or Battlemetrics ID",
entry_tooltip=(
"- IP: Format as IP:Query port\ne.g. 192.168.1.1:27016\n"
"- IP: format as IP:Query port\ne.g. 192.168.1.1:27016\n"
"- Battlemetrics: numeric server ID\ne.g. 123456"
),
add_tooltip="Add to my servers",
connect_tooltip="Connect to this server",
)
@dataclass(slots=True, frozen=True)
class FavPanel:
heading: str
no_fav: str
fav_panel = FavPanel(
heading="Favorite server",
no_fav="None set. Right click a server and select 'Set favorite' to set.",
)
@dataclass(slots=True, frozen=True)
class LanPanel:
heading: str
default_button: str
custom_button: str
scan_button: str
placeholder: str
entry_tooltip: str
lan_panel = LanPanel(
heading="LAN query port",
default_button="Default port (27016)",
custom_button="Custom port",
scan_button="Scan",
placeholder="Enter the query port (1-65535)",
entry_tooltip="Specify the port to search for DayZ servers on the local network"
)
distance_suffix = "Distance: calculating..."
dialog_error = "ERROR"

View File

@ -1,4 +1,4 @@
from typing import Self, TYPE_CHECKING
from typing import Literal, Self, TYPE_CHECKING
from dzgui.util.strings import atomic_buttons, connect_panel
from dzgui.const.constants import (
@ -12,22 +12,24 @@ from dzgui.const.constants import (
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa E402
from gi.repository import Gtk, GLib # noqa E402
if TYPE_CHECKING:
from GLib import SOURCE_REMOVE
from dzgui.controllers.mc import Controller
class Icon(Gtk.Image):
def __init__(self, name: str, l_margin=0) -> None:
def __init__(self, name: str, l_margin: int = 0) -> None:
super().__init__(icon_name=name,
icon_size=Gtk.IconSize.BUTTON,
margin_start=l_margin,
ypad=2,
)
class LargeIcon(Gtk.Image):
def __init__(self, name: str, l_margin=5) -> None:
def __init__(self, name: str, l_margin: int = 5) -> None:
super().__init__(icon_name=name,
icon_size=Gtk.IconSize.LARGE_TOOLBAR,
margin_start=l_margin
@ -40,6 +42,7 @@ class IconButton(Gtk.Button):
self.icon = Icon(icon, l_margin=margin)
self.set_image(self.icon)
self.set_image_position(Gtk.PositionType.RIGHT)
self.set_focus_on_click(False)
class IconTextButton(IconButton):
@ -47,13 +50,10 @@ class IconTextButton(IconButton):
super().__init__(icon, margin=5)
self.set_label(label)
self.set_focus_on_click(False)
class LargeIconTextButton(IconButton):
def __init__(self, icon: str, label: str) -> None:
super().__init__(icon)
self.set_label(label)
self.set_image(LargeIcon(icon))
@ -77,22 +77,60 @@ class WebButton(IconTextButton):
class RefreshButton(IconTextButton):
def __init__(self) -> None:
super().__init__(icon=REFRESH_ICON, label=atomic_buttons.refresh)
def __init__(self, controller: "Controller") -> None:
super().__init__(
icon=REFRESH_ICON,
label=atomic_buttons.refresh,
)
self.controller = controller
self.time = 30
self.set_margin_top(10)
self.set_margin_start(80)
self.set_margin_end(80)
self.set_tooltip_text(atomic_buttons.refresh_tooltip)
self.connect("clicked", self._on_refresh_clicked)
def _on_refresh_clicked(self, button: Self) -> None:
self.set_sensitive(False)
self.show_time(True)
GLib.timeout_add_seconds(1, self.decrement)
self.controller.refresh_tree()
def decrement(self) -> "SOURCE_REMOVE" | Literal[True]:
self.time -= 1
if self.time == 0:
self.time = 30
self.show_time(False)
self.set_sensitive(True)
return GLib.SOURCE_REMOVE
self.show_time(True)
return True
def show_time(self, state: bool) -> None:
if state is True:
self.set_label(f"{atomic_buttons.refresh} ({str(self.time)})")
else:
self.set_label(atomic_buttons.refresh)
class KeysButton(IconTextButton):
def __init__(self) -> None:
def __init__(self, controller: "Controller") -> None:
super().__init__(icon=INPUT_KEYBOARD, label=atomic_buttons.keys)
self.controller = controller
self.set_margin_top(10)
self.set_margin_start(80)
self.set_margin_end(80)
self.set_tooltip_text(atomic_buttons.keys_tooltip)
self.connect("clicked", self._on_keys_clicked)
def _on_keys_clicked(self, button: Gtk.Button) -> None:
self.controller.open_keybindings()
class SteamConnectButton(LargeIconTextButton):
def __init__(self) -> None:

View File

@ -2,8 +2,9 @@ from typing import TYPE_CHECKING
from dzgui.api.servers import validate_ip
from dzgui.util.css import add_class, remove_class
from dzgui.util.strings import connect_panel
from dzgui.util.strings import connect_panel, fav_panel, lan_panel
from dzgui.views.components.buttons import AddButton, ClipboardButton, SteamConnectButton
from dzgui.views.components.entry import IpEntry, PortEntry
from dzgui.views.components.labels import BoldLabel
import gi
@ -21,29 +22,70 @@ class LanPanel(Gtk.Frame):
super().__init__(margin_top=10, margin_bottom=5)
# TODO: hide lan panel on other tabs
# TODO: strings
label = BoldLabel("LAN query port")
label = BoldLabel(lan_panel.heading)
self.set_label_widget(label)
radio1 = Gtk.RadioButton.new_with_label(None, "Default port (27016)")
radio2 = Gtk.RadioButton.new_with_label_from_widget(radio1, "Custom port")
radio1 = Gtk.RadioButton.new_with_label(None, lan_panel.default_button)
radio2 = Gtk.RadioButton.new_with_label_from_widget(radio1, lan_panel.custom_button)
self.entry = Gtk.Entry(placeholder_text="Enter the query port (1-65535)", sensitive=False, hexpand=True)
self.button = Gtk.Button(label="Scan")
self.entry = PortEntry(controller)
self.scan = Gtk.Button(label=lan_panel.scan_button)
self.entry.connect("string_validated", self._on_port_validated)
radio1.connect("toggled", self._on_radio_toggled)
self.grid = Gtk.Grid(margin=10, vexpand=False, column_spacing=15, row_spacing=5)
self.grid.attach(radio1, 0, 0, COLS, ROWS)
# TODO: expression to attach in a line
self.grid.attach_next_to(radio2, radio1, Gtk.PositionType.RIGHT, COLS, ROWS)
self.grid.attach_next_to(self.entry, radio2, Gtk.PositionType.RIGHT, COLS, ROWS)
self.grid.attach_next_to(self.button, self.entry, Gtk.PositionType.RIGHT, COLS, ROWS)
self.grid.attach_next_to(self.scan, self.entry, Gtk.PositionType.RIGHT, COLS, ROWS)
self.add(self.grid)
def _on_radio_toggled(self, button: Gtk.RadioButton) -> None:
self.entry.set_sensitive(not button.get_active())
# TODO: validate custom port entry
if len(self.entry.get_text()) < 1:
self.scan.set_sensitive(False)
def _on_port_validated(self, entry: Gtk.Entry, state: bool) -> None:
self.scan.set_sensitive(state)
class FavPanel(Gtk.Frame):
def __init__(self, controller: "Controller") -> None:
super().__init__(margin_top=10, margin_bottom=5)
label = BoldLabel(fav_panel.heading)
self.set_label_widget(label)
self.controller = controller
# TODO: improve upon this, cache fav ip when changed globally
user_fav, self.fav_ip = self.controller.get_favorite()
server_name = f"{user_fav} ({self.fav_ip})" if user_fav is not None else connect_panel.no_fav
self.fav_label = Gtk.Label(label=server_name, track_visited_links=False, halign=Gtk.Align.START, hexpand=True)
scrollable_label = Gtk.ScrolledWindow()
scrollable_label.add(self.fav_label)
self.fav_button = SteamConnectButton()
if server_name is None:
self.fav_button.set_sensitive(False)
copy = ClipboardButton(self.controller, self.get_fav_ip())
# FIXME: height of connect buttons is not equivalent
grid = Gtk.Grid(margin=10, vexpand=False, column_spacing=15, row_spacing=5)
grid.attach(scrollable_label, 0, 0, 3, ROWS)
grid.attach_next_to(copy, scrollable_label, Gtk.PositionType.RIGHT, COLS, ROWS)
grid.attach_next_to(self.fav_button, copy, Gtk.PositionType.RIGHT, COLS, ROWS)
self.add(grid)
def get_fav_ip(self) -> str:
return self.fav_ip
def set_fav_label(self, text: str) -> None:
# TODO: called by controller when changing fav
self.fav_label.set_text(text)
class AddPanel(Gtk.Frame):
def __init__(self, controller: "Controller") -> None:
@ -60,59 +102,19 @@ class AddPanel(Gtk.Frame):
self.conn_server.set_sensitive(False)
self.add_server.set_sensitive(False)
self.entry = Gtk.Entry(
placeholder_text=connect_panel.placeholder,
hexpand=True,
tooltip_text=connect_panel.entry_tooltip
)
self.entry.connect("key-press-event", self._on_entry_keypress)
self.entry.connect("changed", self._on_text_changed)
self.entry = IpEntry(controller)
self.entry.connect("string_validated", self._on_ip_validated)
self.grid = Gtk.Grid(margin=10, vexpand=False, column_spacing=15, row_spacing=5)
self.grid.attach(self.entry, 0, 0, COLS, ROWS)
els = (
(self.add_server, self.entry, Gtk.PositionType.RIGHT, COLS, ROWS),
(self.conn_server, self.add_server, Gtk.PositionType.RIGHT, COLS, ROWS),
)
for el, sibling, pos, h_span, v_span in els:
self.grid.attach_next_to(el, sibling, pos, h_span, v_span)
self.grid.attach_next_to(self.add_server, self.entry, Gtk.PositionType.RIGHT, COLS, ROWS)
self.grid.attach_next_to(self.conn_server, self.add_server, Gtk.PositionType.RIGHT, COLS, ROWS)
self.add(self.grid)
def mark_valid(self) -> None:
self.conn_server.set_sensitive(True)
self.add_server.set_sensitive(True)
remove_class(self.entry, self.classname)
def mark_invalid(self) -> None:
self.conn_server.set_sensitive(False)
self.add_server.set_sensitive(False)
add_class(self.entry, self.classname)
def _on_text_changed(self, entry: Gtk.Entry) -> None:
# TODO: on submission, strip whitespace and newlines
text = entry.get_text()
if len(text) < 1:
self.conn_server.set_sensitive(False)
self.add_server.set_sensitive(False)
remove_class(entry, self.classname)
return
try:
validate_ip(text)
self.mark_valid()
except Exception:
if text.isdigit():
self.mark_valid()
else:
self.mark_invalid()
def _on_entry_keypress(self, entry: Gtk.Entry, event: Gdk.EventKey) -> None:
if event.keyval == Gdk.KEY_Escape:
# NOTE: unselect text
entry.select_region(0, 0)
self.controller.grab_active_treeview()
def _on_ip_validated(self, entry: Gtk.Entry, state: bool) -> None:
self.conn_server.set_sensitive(state)
self.add_server.set_sensitive(state)
class ConnectPanel(Gtk.Box):
@ -121,39 +123,9 @@ class ConnectPanel(Gtk.Box):
self.controller = controller
user_fav, self.fav_ip = self.controller.get_favorite()
self.lan = LanPanel(controller)
self.fav = FavPanel(controller)
self.add_panel = AddPanel(controller)
server_name = f"{user_fav} ({self.fav_ip})" if user_fav is not None else connect_panel.no_fav
self.fav_label = Gtk.Label(label=server_name, track_visited_links=False, halign=Gtk.Align.START, hexpand=True)
scrollable_label = Gtk.ScrolledWindow()
scrollable_label.add(self.fav_label)
self.fav_button = SteamConnectButton()
self.fav_edit = Gtk.Button(label=connect_panel.edit)
if server_name is None:
self.fav_button.set_sensitive(False)
self.lan = LanPanel(self.controller)
self.add_panel = AddPanel(self.controller)
label = BoldLabel("Favorite server")
frame = Gtk.Frame(margin_top=10, margin_bottom=5, label_widget=label)
b = ClipboardButton(self.controller, self.get_fav_ip())
# FIXME: height of connect buttons is not equivalent
grid = Gtk.Grid(margin=10, vexpand=False, column_spacing=15, row_spacing=5)
grid.attach(scrollable_label, 0, 0, 3, ROWS)
grid.attach_next_to(b, scrollable_label, Gtk.PositionType.RIGHT, COLS, ROWS)
grid.attach_next_to(self.fav_button, b, Gtk.PositionType.RIGHT, COLS, ROWS)
frame.add(grid)
for el in self.lan, frame, self.add_panel:
for el in self.lan, self.fav, self.add_panel:
self.add(el)
def get_fav_ip(self) -> str:
return self.fav_ip
def set_fav_label(self, text: str) -> None:
# TODO: called by controller when changing fav
self.fav_label.set_text(text)

View File

@ -1,27 +1,23 @@
from typing import Literal, TYPE_CHECKING
import gi # noqa E402
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk, GLib
from dzgui.const.enum import Preferences
from dzgui.views.components.buttonbox import ButtonBox
from dzgui.views.components.filter_panel import FilterPanel
from dzgui.views.components.mod_panel import ModSelectionPanel
from dzgui.views.components.buttons import RefreshButton, KeysButton
from dzgui.const.constants import NO_EXPAND, NO_FILL, EXPAND, FILL, INPUT_KEYBOARD, NO_PADDING
from dzgui.const.constants import NO_EXPAND, NO_FILL, FILL, NO_PADDING
from dzgui.util.strings import atomic_buttons
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib # noqa E402
if TYPE_CHECKING:
from GLib import SOURCE_REMOVE
# TODO: refactor depends on ServerTreeView
class RightPanel(Gtk.Box):
def __init__(self, controller):
super().__init__(spacing=6, orientation=Gtk.Orientation.VERTICAL)
self.time = 30
self.controller = controller
self.controller.register_widget("right_panel", self)
@ -29,15 +25,8 @@ class RightPanel(Gtk.Box):
self.filters_vbox = FilterPanel(controller)
self.sel_panel = ModSelectionPanel(controller)
self.refresh_button = RefreshButton()
# TODO: consolidate into class
#self.refresh_button.set_tooltip_text(refresh_tooltip)
self.refresh_button.set_focus_on_click(False)
self.refresh_button.connect("clicked", self._on_refresh_clicked)
self.keys = KeysButton()
self.keys.connect("clicked", self._on_keys_clicked)
#self.keys.set_focus_on_click(False)
self.refresh_button = RefreshButton(controller)
self.keys = KeysButton(controller)
for el in self.button_vbox, self.keys, self.filters_vbox, self.refresh_button:
self.pack_start(el, NO_EXPAND, FILL, NO_PADDING)
@ -48,29 +37,11 @@ class RightPanel(Gtk.Box):
def reinit_maps(self, rows: list) -> None:
self.controller.reinit_map_store()
# TODO: communicate with controller
#controller.clear_map_store()
#self.controller.clear_map_store()
#map_store.append(["All maps"])
self.selected = "All maps"
self.filters_vbox.set_unique_maps(rows)
def decrement(self) -> "SOURCE_REMOVE" | Literal[True]:
self.time -= 1
if self.time == 0:
self.time = 30
self.refresh_button.set_label(refresh)
self.refresh_button.set_sensitive(True)
return GLib.SOURCE_REMOVE
# TODO: abstract this formatting logic
self.refresh_button.set_label(f"{atomic_buttons.refresh} ({str(self.time)})")
return True
def _on_refresh_clicked(self, button: RefreshButton) -> None:
# TODO: could be internal to refresh button class
self.refresh_button.set_sensitive(False)
self.refresh_button.set_label(f"{atomic_buttons.refresh} ({str(self.time)})")
GLib.timeout_add_seconds(1, self.decrement)
self.controller.refresh_tree()
# TODO: reference
#def _on_ping_clicked(self, button: Gtk.Button) -> None:
# block_signals()
@ -103,8 +74,5 @@ class RightPanel(Gtk.Box):
# thread = threading.Thread(target=_update_pings, args=())
# thread.start()
def _on_keys_clicked(self, button: Gtk.Button) -> None:
self.controller.open_keybindings()
def focus_button_box(self) -> None:
self.button_vbox.buttons[0].grab_focus()

View File

@ -211,7 +211,8 @@ class ServerTreeView(TreeView):
case Gdk.KEY_m:
self.controller.mediator.grid.right_panel.filters_vbox.maps_entry.grab_focus()
case Gdk.KEY_i:
self.controller.mediator.grid.conpan.entry.grab_focus()
# TODO:
self.controller.mediator.grid.conpan.add_panel.entry.grab_focus()
else:
match event.keyval:
case Gdk.KEY_l | Gdk.KEY_Right: