package com.ts3client.ui; import com.ts3client.config.Bookmark; import com.ts3client.config.Bookmarks; import com.ts3client.config.IdentityStore; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.DefaultListModel; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPasswordField; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JTextField; import javax.swing.ListSelectionModel; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.Frame; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.util.function.Consumer; /** * Manage saved servers. Two columns: the bookmark list on the left, the * selected bookmark's settings on the right. Edits are applied to the selected * bookmark as soon as the selection changes or the dialog is closed. */ public final class BookmarksDialog extends JDialog { private final Bookmarks bookmarks; private final Consumer onConnect; private final Runnable onChanged; private final DefaultListModel listModel = new DefaultListModel<>(); private final JList list = new JList<>(listModel); private final JTextField label = new JTextField(); private final JTextField address = new JTextField(); private final JTextField port = new JTextField(); private final JTextField nick = new JTextField(); private final JPasswordField password = new JPasswordField(); private final JTextField channel = new JTextField(); private final JPasswordField channelPassword = new JPasswordField(); private final JCheckBox connectOnStartup = new JCheckBox("Connect on startup"); private final IdentityChooser identity; private final JPanel details; /** Bookmark the form currently shows, so edits can be flushed back into it. */ private Bookmark editing; public BookmarksDialog(Frame owner, Bookmarks bookmarks, IdentityStore identities, Consumer onConnect, Runnable onChanged) { super(owner, "Manage Bookmarks", true); this.bookmarks = bookmarks; this.onConnect = onConnect; this.onChanged = onChanged; this.identity = new IdentityChooser(identities, true, ""); list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); list.setVisibleRowCount(12); list.addListSelectionListener(e -> { if (!e.getValueIsAdjusting()) onSelectionChanged(); }); details = buildDetails(); JScrollPane listScroll = new JScrollPane(list); listScroll.setPreferredSize(new Dimension(180, 320)); JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, listScroll, details); split.setBorder(BorderFactory.createEmptyBorder(8, 8, 4, 8)); split.setResizeWeight(0.3); split.setContinuousLayout(true); getContentPane().setLayout(new BorderLayout()); getContentPane().add(split, BorderLayout.CENTER); getContentPane().add(buildButtons(), BorderLayout.SOUTH); reload(); if (!listModel.isEmpty()) list.setSelectedIndex(0); else showBookmark(null); Dialogs.closeOnEscape(this, this::closeDialog); setDefaultCloseOperation(DISPOSE_ON_CLOSE); addWindowListener(new java.awt.event.WindowAdapter() { @Override public void windowClosing(java.awt.event.WindowEvent e) { commit(); persist(); } }); setSize(new Dimension(640, 400)); setLocationRelativeTo(owner); } // ---- UI construction ---- private JPanel buildDetails() { JPanel panel = new JPanel(new GridBagLayout()); panel.setBorder(BorderFactory.createEmptyBorder(0, 8, 0, 0)); channel.setToolTipText("Channel path, e.g. \"Lobby/Games\", or a channel id. " + "Leave empty for the server's default channel."); connectOnStartup.setToolTipText("Connect to this server automatically when the client starts. " + "Several bookmarks may be flagged; each opens its own tab."); int row = 0; addRow(panel, row++, "Label:", label); addRow(panel, row++, "Address:", address); addRow(panel, row++, "Port:", port); addRow(panel, row++, "Nickname:", nick); addRow(panel, row++, "Password:", password); addRow(panel, row++, "Default channel:", channel); addRow(panel, row++, "Channel password:", channelPassword); addRow(panel, row++, "Identity:", identity); addRow(panel, row++, "", connectOnStartup); GridBagConstraints filler = new GridBagConstraints(); filler.gridx = 0; filler.gridy = row; filler.weighty = 1; filler.fill = GridBagConstraints.VERTICAL; panel.add(Box.createVerticalGlue(), filler); return panel; } private void addRow(JPanel panel, int row, String text, java.awt.Component field) { GridBagConstraints c = new GridBagConstraints(); c.gridy = row; c.insets = new Insets(2, 0, 2, 4); c.anchor = GridBagConstraints.WEST; c.gridx = 0; panel.add(new JLabel(text), c); c.gridx = 1; c.weightx = 1; c.fill = GridBagConstraints.HORIZONTAL; panel.add(field, c); } private JPanel buildButtons() { JPanel buttons = new JPanel(); buttons.setLayout(new BoxLayout(buttons, BoxLayout.X_AXIS)); buttons.setBorder(BorderFactory.createEmptyBorder(0, 8, 8, 8)); addButton(buttons, "Add", this::addBookmark); addButton(buttons, "Remove", this::removeSelected); buttons.add(Box.createHorizontalGlue()); addButton(buttons, "Connect", this::connectSelected); addButton(buttons, "Close", this::closeDialog); return buttons; } private void addButton(JPanel panel, String text, Runnable action) { JButton b = new JButton(text); b.addActionListener(e -> action.run()); panel.add(b); panel.add(Box.createHorizontalStrut(4)); } // ---- Form <-> model ---- private void onSelectionChanged() { commit(); showBookmark(list.getSelectedValue()); } private void showBookmark(Bookmark b) { editing = b; setEnabledRecursively(details, b != null); label.setText(b == null ? "" : nullToEmpty(b.label)); address.setText(b == null ? "" : nullToEmpty(b.address)); port.setText(b == null ? "" : Integer.toString(b.port)); nick.setText(b == null ? "" : nullToEmpty(b.nickname)); password.setText(b == null ? "" : nullToEmpty(b.password)); channel.setText(b == null ? "" : nullToEmpty(b.channel)); channelPassword.setText(b == null ? "" : nullToEmpty(b.channelPassword)); connectOnStartup.setSelected(b != null && b.connectOnStartup); identity.setSelectedIdentityId(b == null ? "" : nullToEmpty(b.identityId)); } /** Writes the form back into the bookmark it was loaded from. */ private void commit() { if (editing == null) return; editing.label = label.getText().trim(); editing.address = address.getText().trim(); try { editing.port = Integer.parseInt(port.getText().trim()); } catch (NumberFormatException e) { editing.port = 9987; } editing.nickname = nick.getText().trim(); editing.password = new String(password.getPassword()); editing.channel = channel.getText().trim(); editing.channelPassword = new String(channelPassword.getPassword()); editing.identityId = identity.getSelectedIdentityId(); editing.connectOnStartup = connectOnStartup.isSelected(); list.repaint(); } // ---- Actions ---- private void addBookmark() { commit(); Bookmark b = new Bookmark("New bookmark", "", 9987, System.getProperty("user.name", "TS3J User"), ""); bookmarks.add(b); reload(); list.setSelectedValue(b, true); persist(); label.requestFocusInWindow(); } private void removeSelected() { int idx = list.getSelectedIndex(); if (idx < 0) return; editing = null; // dropped, nothing to flush back bookmarks.remove(idx); reload(); if (!listModel.isEmpty()) list.setSelectedIndex(Math.min(idx, listModel.size() - 1)); else showBookmark(null); persist(); } private void connectSelected() { commit(); Bookmark b = list.getSelectedValue(); if (b == null) return; if (b.address == null || b.address.isBlank()) { JOptionPane.showMessageDialog(this, "Address is required."); return; } persist(); onConnect.accept(b); dispose(); } private void closeDialog() { commit(); persist(); dispose(); } private void reload() { listModel.clear(); for (Bookmark b : bookmarks.all()) listModel.addElement(b); } private void persist() { bookmarks.save(); if (onChanged != null) onChanged.run(); } private static void setEnabledRecursively(java.awt.Container container, boolean enabled) { for (java.awt.Component c : container.getComponents()) { c.setEnabled(enabled); if (c instanceof java.awt.Container child) setEnabledRecursively(child, enabled); } } private static String nullToEmpty(String s) { return s == null ? "" : s; } }