#1060 Adds Alias List delete and rename operations to playlist editor (#2038)

Original code changes implemented by @whelgeson

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
This commit is contained in:
Denny Sheirer
2024-10-21 05:34:06 -04:00
committed by GitHub
parent 378841b408
commit 66a0b2543b
6 changed files with 378 additions and 37 deletions

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -63,6 +63,69 @@ public class AliasModel
return mAliasListNames;
}
/**
* Clears and reloads the list of alias list names from the current set of aliases.
*/
public void refreshAliasListNames()
{
mAliasListNames.clear();
for(Alias alias : mAliases)
{
String aliasListName = alias.getAliasListName();
if(aliasListName != null && !aliasListName.isEmpty() && !mAliasListNames.contains(aliasListName))
{
mAliasListNames.add(aliasListName);
}
}
}
/**
* Renames the alias list across the set of aliases.
* @param oldName currently used by the alias
* @param newName to apply to the alias
*/
public void renameAliasList(String oldName, String newName)
{
if(oldName == null || oldName.isEmpty() || newName == null || newName.isEmpty())
{
return;
}
mAliases.stream().filter(alias -> alias.getAliasListName().equals(oldName)).forEach(alias -> alias.setAliasListName(newName));
}
/**
* Deletes any aliases that have the alias list name
* @param aliasListName to delete
*/
public void deleteAliasList(String aliasListName)
{
if(aliasListName == null || aliasListName.isEmpty())
{
return;
}
mAliases.removeIf(alias -> alias.getAliasListName().equals(aliasListName));
mAliasListMap.remove(aliasListName);
}
/**
* Adds the list of alias list names to the currently managed set of alias list names.
* @param aliasListNames to add
*/
public void addAliasListNames(List<String> aliasListNames)
{
for(String aliasListName : aliasListNames)
{
if(!mAliasListNames.contains(aliasListName))
{
mAliasListNames.add(aliasListName);
}
}
}
/**
* Unmodifiable list of all aliases currently in the model
*/

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.controller.channel;
@@ -26,15 +23,13 @@ import io.github.dsheirer.controller.channel.Channel.ChannelType;
import io.github.dsheirer.controller.channel.ChannelEvent.Event;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* Channel Model
*/
@@ -55,6 +50,58 @@ public class ChannelModel implements Listener<ChannelEvent>
mTrafficChannels.addListener(changeListener);
}
/**
* Get a deduplicated list of alias list names from across the channel configurations.
* @return list of alias list names.
*/
public List<String> getAliasListNames()
{
List<String> aliasListNames = new ArrayList<>();
for(Channel channel : mChannels)
{
String aliasListName = channel.getAliasListName();
if(aliasListName != null && !aliasListName.isEmpty() && !aliasListNames.contains(aliasListName))
{
aliasListNames.add(aliasListName);
}
}
return aliasListNames;
}
/**
* Renames the alias list across the set of aliases.
* @param oldName currently used by the alias
* @param newName to apply to the alias
*/
public void renameAliasList(String oldName, String newName)
{
if(oldName == null || oldName.isEmpty() || newName == null || newName.isEmpty())
{
return;
}
mChannels.stream().filter(channel -> channel.getAliasListName().equals(oldName))
.forEach(channel -> channel.setAliasListName(newName));
}
/**
* Deletes any aliases that have the alias list name
* @param aliasListName to delete
*/
public void deleteAliasList(String aliasListName)
{
if(aliasListName == null || aliasListName.isEmpty())
{
return;
}
mChannels.stream().filter(channel -> channel.getAliasListName().equals(aliasListName))
.forEach(channel -> channel.setAliasListName(null));
}
/**
* Observable list of channel configurations managed by this model
*/
@@ -109,6 +156,45 @@ public class ChannelModel implements Listener<ChannelEvent>
return null;
}
/**
* Renames the alias list across all channels.
* @param existing alias list name
* @param renamed alias list name.
*/
public void renameAliaslistForChannels(String existing, String renamed)
{
if(existing == null || existing.isEmpty() || renamed == null || renamed.isEmpty())
{
return;
}
mChannels.forEach(channel -> {
if(channel.getAliasListName() != null && channel.getAliasListName().equals(existing))
{
channel.setAliasListName(renamed);
}
});
}
/**
* Removes the alias list name from any channels.
* @param name to delete
*/
public void deleteAliasListName(String name)
{
if(name == null || name.isEmpty())
{
return;
}
mChannels.forEach(channel -> {
if(channel.getAliasListName() != null && channel.getAliasListName().equals(name))
{
channel.setAliasListName(null);
}
});
}
/**
* Returns a list of unique system values from across the channel set
*/

View File

@@ -0,0 +1,37 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.gui.playlist;
/**
* Interface for editors that use alias list controls that can trigger when the playlist manager updates the observable
* set of alias list names and cause the editor to prompt the user to save changes.
*
* Implementors should clear any currently selected/editing element that could be impacted by a behind-the-scenes update
* to the list of alias list names such as delete or rename operations.
*/
public interface IAliasListRefreshListener
{
/**
* Preparate foran alias list refresh. For alias editors, this can be a clear the currently selected
* alias so that if the value is changed, the editor doesn't think that the user has modified the alias and
* prompt the user to save the changes.
*/
void prepareForAliasListRefresh();
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -25,6 +25,7 @@ import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.gui.control.MaxLengthUnaryOperator;
import io.github.dsheirer.gui.playlist.Editor;
import io.github.dsheirer.gui.playlist.IAliasListRefreshListener;
import io.github.dsheirer.icon.Icon;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
@@ -41,7 +42,6 @@ import javafx.collections.transformation.SortedList;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
@@ -76,7 +76,7 @@ import org.slf4j.LoggerFactory;
/**
* Editor for aliases
*/
public class AliasConfigurationEditor extends SplitPane
public class AliasConfigurationEditor extends SplitPane implements IAliasListRefreshListener
{
private static final Logger mLog = LoggerFactory.getLogger(AliasConfigurationEditor.class);
@@ -89,6 +89,7 @@ public class AliasConfigurationEditor extends SplitPane
private Label mPlaceholderLabel;
private Button mNewAliasButton;
private Button mDeleteAliasButton;
private Button mRenameAliasButton;
private Button mCloneAliasButton;
private MenuButton mMoveToAliasButton;
private VBox mButtonBox;
@@ -96,13 +97,20 @@ public class AliasConfigurationEditor extends SplitPane
private TextField mSearchField;
private ComboBox<String> mAliasListNameComboBox;
private Button mNewAliasListButton;
private Button mDeleteAliasListButton;
private FilteredList<Alias> mAliasFilteredList;
private SortedList<Alias> mAliasSortedList;
private AliasPredicate mAliasPredicate;
/**
* Constructs an instance
* @param playlistManager for playlist operations
* @param userPreferences for user preferences
*/
public AliasConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
{
mPlaylistManager = playlistManager;
mPlaylistManager.addAliasListRefreshListener(this);
mUserPreferences = userPreferences;
VBox leftBox = new VBox();
@@ -118,6 +126,15 @@ public class AliasConfigurationEditor extends SplitPane
getItems().addAll(topBox, getAliasItemEditor());
}
/**
* Prepares for an alias list refresh by clearing the currently selected alias item from the editor.
*/
@Override
public void prepareForAliasListRefresh()
{
getAliasTableView().getSelectionModel().select(null);
}
/**
* Request to show the specified alias in the editor.
* <p>
@@ -167,18 +184,17 @@ public class AliasConfigurationEditor extends SplitPane
alert.setTitle("Save Changes");
alert.setHeaderText("Alias configuration has been modified");
alert.setContentText("Do you want to save these changes?");
alert.initOwner(((Node) getButtonBox()).getScene().getWindow());
alert.initOwner((getButtonBox()).getScene().getWindow());
//Workaround for JavaFX KDE on Linux bug in FX 10/11: https://bugs.openjdk.java.net/browse/JDK-8179073
alert.setResizable(true);
alert.onShownProperty().addListener(e ->
{
alert.onShownProperty().addListener(e -> {
Platform.runLater(() -> alert.setResizable(false));
});
Optional<ButtonType> result = alert.showAndWait();
if(result.get() == ButtonType.YES)
if(result.isPresent() && result.get() == ButtonType.YES)
{
getAliasItemEditor().save();
}
@@ -202,7 +218,7 @@ public class AliasConfigurationEditor extends SplitPane
getAliasBulkEditor().setItem(aliases);
}
getCloneAliasButton().setDisable(aliases.isEmpty() || aliases.size() > 1);
getCloneAliasButton().setDisable(aliases.size() != 1);
getDeleteAliasButton().setDisable(aliases.isEmpty());
getMoveToAliasButton().setDisable(aliases.isEmpty());
}
@@ -248,7 +264,7 @@ public class AliasConfigurationEditor extends SplitPane
searchBox.setAlignment(Pos.BASELINE_RIGHT);
mSearchAndListSelectionBox.getChildren().addAll(listLabel, getAliasListNameComboBox(),
getNewAliasListButton(), searchBox);
getNewAliasListButton(), getRenameAliasListButton(), getDeleteAliasListButton(), searchBox);
}
return mSearchAndListSelectionBox;
@@ -347,6 +363,70 @@ public class AliasConfigurationEditor extends SplitPane
return mNewAliasListButton;
}
private Button getRenameAliasListButton() {
if (mRenameAliasButton == null) {
mRenameAliasButton = new Button("Rename Alias List");
mRenameAliasButton.setOnAction(event -> {
String aliasListName = getAliasListNameComboBox().getSelectionModel().getSelectedItem();
if (aliasListName.equals(AliasModel.NO_ALIAS_LIST)) {
Alert alert = new Alert(Alert.AlertType.ERROR, "", ButtonType.OK);
alert.setTitle("Rename Alias List");
alert.setHeaderText("You cannot rename " + aliasListName + ".");
alert.initOwner((getRenameAliasListButton()).getScene().getWindow());
alert.showAndWait();
return;
}
TextInputDialog dialog = new TextInputDialog();
dialog.setTitle("Renaming Alias List: " + aliasListName);
dialog.setHeaderText("Please enter the new alias list name (max 25 chars).");
dialog.setContentText("Name:");
dialog.getEditor().setTextFormatter(new TextFormatter<String>(new MaxLengthUnaryOperator(25)));
Optional<String> result = dialog.showAndWait();
result.ifPresent(newAliasListName -> {
mPlaylistManager.renameAliasList(aliasListName, newAliasListName);
getAliasListNameComboBox().getSelectionModel().select(newAliasListName);
});
});
}
return mRenameAliasButton;
}
private Button getDeleteAliasListButton() {
if (mDeleteAliasListButton == null) {
mDeleteAliasListButton = new Button("Delete Alias List");
mDeleteAliasListButton.setOnAction(event -> {
String aliasListName = getAliasListNameComboBox().getSelectionModel().getSelectedItem();
if (aliasListName.equals(AliasModel.NO_ALIAS_LIST)) {
Alert alert = new Alert(Alert.AlertType.ERROR, "", ButtonType.OK);
alert.setTitle("Delete Alias List");
alert.setHeaderText("You cannot delete " + aliasListName + ".");
alert.initOwner((getDeleteAliasListButton()).getScene().getWindow());
alert.showAndWait();
return;
}
Alert alert = new Alert(Alert.AlertType.CONFIRMATION, "", ButtonType.NO, ButtonType.YES);
alert.setTitle("Delete Alias List");
alert.setHeaderText("Are you sure you want to delete the alias list " + aliasListName + " and all associated aliases?");
alert.initOwner((getDeleteAliasListButton()).getScene().getWindow());
Optional<ButtonType> result = alert.showAndWait();
if (result.isPresent() && result.get() == ButtonType.YES)
{
mPlaylistManager.deleteAliasList(aliasListName);
}
});
}
return mDeleteAliasListButton;
}
private TableView<Alias> getAliasTableView()
{
if(mAliasTableView == null)
@@ -423,8 +503,7 @@ public class AliasConfigurationEditor extends SplitPane
mAliasTableView.setPlaceholder(getPlaceholderLabel());
mAliasTableView.setItems(getAliasSortedList());
mAliasTableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
mAliasTableView.getSelectionModel().getSelectedItems().addListener((ListChangeListener<Alias>) c ->
{
mAliasTableView.getSelectionModel().getSelectedItems().addListener((ListChangeListener<Alias>)c -> {
Platform.runLater(() -> setAliases(mAliasTableView.getSelectionModel().getSelectedItems()));
});
}
@@ -537,11 +616,11 @@ public class AliasConfigurationEditor extends SplitPane
ButtonType.NO, ButtonType.YES);
alert.setTitle("Delete Alias");
alert.setHeaderText("Are you sure?");
alert.initOwner(((Node) getDeleteAliasButton()).getScene().getWindow());
alert.initOwner((getDeleteAliasButton()).getScene().getWindow());
Optional<ButtonType> result = alert.showAndWait();
if(result.get() == ButtonType.YES)
if(result.isPresent() && result.get() == ButtonType.YES)
{
List<Alias> selectedAliases = new ArrayList<>(getAliasTableView().getSelectionModel().getSelectedItems());
mPlaylistManager.getAliasModel().removeAliases(selectedAliases);

View File

@@ -21,6 +21,7 @@ package io.github.dsheirer.gui.playlist.channel;
import com.google.common.base.Joiner;
import io.github.dsheirer.controller.channel.Channel;
import io.github.dsheirer.gui.playlist.IAliasListRefreshListener;
import io.github.dsheirer.module.decode.DecoderFactory;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.playlist.PlaylistManager;
@@ -73,7 +74,7 @@ import org.slf4j.LoggerFactory;
/**
* JavaFX editor for managing channel configurations.
*/
public class ChannelEditor extends SplitPane implements IFilterProcessor
public class ChannelEditor extends SplitPane implements IFilterProcessor, IAliasListRefreshListener
{
private final static Logger mLog = LoggerFactory.getLogger(ChannelEditor.class);
private PlaylistManager mPlaylistManager;
@@ -104,6 +105,7 @@ public class ChannelEditor extends SplitPane implements IFilterProcessor
public ChannelEditor(PlaylistManager playlistManager, TunerManager tunerManager, UserPreferences userPreferences)
{
mPlaylistManager = playlistManager;
mPlaylistManager.addAliasListRefreshListener(this);
mTunerManager = tunerManager;
mUserPreferences = userPreferences;
mUnknownConfigurationEditor = new UnknownConfigurationEditor(mPlaylistManager, mTunerManager,
@@ -124,6 +126,15 @@ public class ChannelEditor extends SplitPane implements IFilterProcessor
getItems().addAll(topBox, getChannelConfigurationEditor());
}
/**
* Prepares for an alias list refresh by clearing the currently selected channel from the channel table.
*/
@Override
public void prepareForAliasListRefresh()
{
getChannelTableView().getSelectionModel().select(null);
}
/**
* Processes the channel view request.
*

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -33,6 +33,7 @@ import io.github.dsheirer.controller.channel.ChannelProcessingManager;
import io.github.dsheirer.controller.channel.map.ChannelMap;
import io.github.dsheirer.controller.channel.map.ChannelMapModel;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.gui.playlist.IAliasListRefreshListener;
import io.github.dsheirer.icon.IconModel;
import io.github.dsheirer.module.log.EventLogManager;
import io.github.dsheirer.preference.UserPreferences;
@@ -48,6 +49,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -77,6 +79,7 @@ public class PlaylistManager implements Listener<ChannelEvent>
private AtomicBoolean mPlaylistSavePending = new AtomicBoolean();
private ScheduledFuture<?> mPlaylistSaveFuture;
private boolean mPlaylistLoading = false;
private List<IAliasListRefreshListener> mAliasListRefreshListeners = new ArrayList<>();
/**
* Playlist manager - manages all channel configurations, channel maps, and alias lists and handles loading or
@@ -135,6 +138,68 @@ public class PlaylistManager implements Listener<ChannelEvent>
});
}
/**
* Adds the listener to be notified when an alias list refresh operation is about to take place. The listener
* should clear any selected or editing item to prepare for the list of alias list names to be updated so that
* alias list combo boxes won't trigger an editor modified event when the list contents changes.
* @param listener for alias list refresh event.
*/
public void addAliasListRefreshListener(IAliasListRefreshListener listener)
{
mAliasListRefreshListeners.add(listener);
}
/**
* Notifies listeners that the alias list will be refreshed
*/
private void prepareForAliasListRefresh()
{
for(IAliasListRefreshListener editor : mAliasListRefreshListeners)
{
editor.prepareForAliasListRefresh();
}
}
/**
* Refresh the alias list names after a rename or delete operation.
*/
private void refreshAliasListNames()
{
//Do a refresh from the aliases
getAliasModel().refreshAliasListNames();
//Add in the alias list names referred to by the channels.
getAliasModel().addAliasListNames(mChannelModel.getAliasListNames());
}
/**
* Renames alias list references across aliases and channels from the old name to the new name.
*
* Note: this method should be invoked on the JavaFX thread since it will touch observable alias and channel lists.
* @param oldName that is currently used by channels and aliases
* @param newName to apply to channels and aliases
*/
public void renameAliasList(String oldName, String newName)
{
prepareForAliasListRefresh();
getAliasModel().renameAliasList(oldName, newName);
getChannelModel().renameAliasList(oldName, newName);
refreshAliasListNames();
}
/**
* Deletes all aliases that have the alias list name and removes the alias list name from all channels.
*
* Note: this method should be invoked on the JavaFX thread since it will touch observable alias and channel lists.
* @param aliasListName to delete
*/
public void deleteAliasList(String aliasListName)
{
prepareForAliasListRefresh();
getAliasModel().deleteAliasList(aliasListName);
getChannelModel().deleteAliasList(aliasListName);
refreshAliasListNames();
}
/**
* Channel model managed by this playlist manager
*/