From 66a0b2543bb8f0ca62b4da01d2205e3806126d76 Mon Sep 17 00:00:00 2001 From: Denny Sheirer Date: Mon, 21 Oct 2024 05:34:06 -0400 Subject: [PATCH] #1060 Adds Alias List delete and rename operations to playlist editor (#2038) Original code changes implemented by @whelgeson Co-authored-by: Dennis Sheirer --- .../io/github/dsheirer/alias/AliasModel.java | 65 ++++++++- .../controller/channel/ChannelModel.java | 128 +++++++++++++++--- .../playlist/IAliasListRefreshListener.java | 37 +++++ .../alias/AliasConfigurationEditor.java | 105 ++++++++++++-- .../gui/playlist/channel/ChannelEditor.java | 13 +- .../dsheirer/playlist/PlaylistManager.java | 67 ++++++++- 6 files changed, 378 insertions(+), 37 deletions(-) create mode 100644 src/main/java/io/github/dsheirer/gui/playlist/IAliasListRefreshListener.java diff --git a/src/main/java/io/github/dsheirer/alias/AliasModel.java b/src/main/java/io/github/dsheirer/alias/AliasModel.java index b9d09334..1ace3414 100644 --- a/src/main/java/io/github/dsheirer/alias/AliasModel.java +++ b/src/main/java/io/github/dsheirer/alias/AliasModel.java @@ -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 aliasListNames) + { + for(String aliasListName : aliasListNames) + { + if(!mAliasListNames.contains(aliasListName)) + { + mAliasListNames.add(aliasListName); + } + } + } + /** * Unmodifiable list of all aliases currently in the model */ diff --git a/src/main/java/io/github/dsheirer/controller/channel/ChannelModel.java b/src/main/java/io/github/dsheirer/controller/channel/ChannelModel.java index 59df59a0..c18c1700 100644 --- a/src/main/java/io/github/dsheirer/controller/channel/ChannelModel.java +++ b/src/main/java/io/github/dsheirer/controller/channel/ChannelModel.java @@ -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 - * * ***************************************************************************** + * 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 + * **************************************************************************** */ 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 mTrafficChannels.addListener(changeListener); } + /** + * Get a deduplicated list of alias list names from across the channel configurations. + * @return list of alias list names. + */ + public List getAliasListNames() + { + List 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 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 */ diff --git a/src/main/java/io/github/dsheirer/gui/playlist/IAliasListRefreshListener.java b/src/main/java/io/github/dsheirer/gui/playlist/IAliasListRefreshListener.java new file mode 100644 index 00000000..fef714f4 --- /dev/null +++ b/src/main/java/io/github/dsheirer/gui/playlist/IAliasListRefreshListener.java @@ -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 + * **************************************************************************** + */ + +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(); +} diff --git a/src/main/java/io/github/dsheirer/gui/playlist/alias/AliasConfigurationEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/alias/AliasConfigurationEditor.java index 2d53c2ef..9106ed8a 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/alias/AliasConfigurationEditor.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/alias/AliasConfigurationEditor.java @@ -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 mAliasListNameComboBox; private Button mNewAliasListButton; + private Button mDeleteAliasListButton; private FilteredList mAliasFilteredList; private SortedList 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. *

@@ -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 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(new MaxLengthUnaryOperator(25))); + Optional 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 result = alert.showAndWait(); + if (result.isPresent() && result.get() == ButtonType.YES) + { + mPlaylistManager.deleteAliasList(aliasListName); + } + }); + } + + return mDeleteAliasListButton; + } + + private TableView 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) c -> - { + mAliasTableView.getSelectionModel().getSelectedItems().addListener((ListChangeListener)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 result = alert.showAndWait(); - if(result.get() == ButtonType.YES) + if(result.isPresent() && result.get() == ButtonType.YES) { List selectedAliases = new ArrayList<>(getAliasTableView().getSelectionModel().getSelectedItems()); mPlaylistManager.getAliasModel().removeAliases(selectedAliases); diff --git a/src/main/java/io/github/dsheirer/gui/playlist/channel/ChannelEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/channel/ChannelEditor.java index dddbed26..bb437a63 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/channel/ChannelEditor.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/channel/ChannelEditor.java @@ -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. * diff --git a/src/main/java/io/github/dsheirer/playlist/PlaylistManager.java b/src/main/java/io/github/dsheirer/playlist/PlaylistManager.java index c5738170..8ecd8404 100644 --- a/src/main/java/io/github/dsheirer/playlist/PlaylistManager.java +++ b/src/main/java/io/github/dsheirer/playlist/PlaylistManager.java @@ -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 private AtomicBoolean mPlaylistSavePending = new AtomicBoolean(); private ScheduledFuture mPlaylistSaveFuture; private boolean mPlaylistLoading = false; + private List 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 }); } + /** + * 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 */