diff --git a/Quelea/changelogs/changelog-2024.0.txt b/Quelea/changelogs/changelog-2024.0.txt
index 858988c89..3f3317b45 100644
--- a/Quelea/changelogs/changelog-2024.0.txt
+++ b/Quelea/changelogs/changelog-2024.0.txt
@@ -8,5 +8,6 @@ quelea (2024.0) stable
* Prevent blacked stage view option
* Logo display now supports video files
* Unsupported videos now have clear unsupported preview image
+ * Allow multiple selection in song database
* Critical fix: Using the French language no longer crashes Quelea when opening the options dialog
* Various minor bugfixes
\ No newline at end of file
diff --git a/Quelea/languages/gb.lang b/Quelea/languages/gb.lang
index 1d18f0d4b..a8b749c02 100644
--- a/Quelea/languages/gb.lang
+++ b/Quelea/languages/gb.lang
@@ -345,6 +345,7 @@ cant.save.schedule.text=Sorry, an error occurred and the schedule couldn't be sa
error.removing.song.db=There was an error removing the song from the database.
confirm.remove.text=Confirm remove
confirm.remove.question=Really remove $1 from the database? This action cannot be undone.
+confirm.remove.bulk.question=Really remove $1 songs from the database? This action cannot be undone.
quick.edit.text=Quick Edit
library.preview.song.text=Preview song
video.error.unsupported=Sorry, the video file you selected isn't supported.
diff --git a/Quelea/src/main/java/org/quelea/data/db/SongManager.java b/Quelea/src/main/java/org/quelea/data/db/SongManager.java
index 616e384d8..90c0ea341 100644
--- a/Quelea/src/main/java/org/quelea/data/db/SongManager.java
+++ b/Quelea/src/main/java/org/quelea/data/db/SongManager.java
@@ -26,6 +26,8 @@
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
import javafx.application.Platform;
import org.hibernate.ObjectNotFoundException;
import org.hibernate.Session;
@@ -42,6 +44,7 @@
/**
* Manage songs persistent operations.
*
+ *
* @author Michael
*/
public final class SongManager {
@@ -66,6 +69,7 @@ private SongManager() {
* Get the singleton instance of this class. Return null if there was an
* error with the database.
*
+ *
* @return the singleton instance of this class.
*/
public static synchronized SongManager get() {
@@ -82,6 +86,7 @@ public static synchronized SongManager get() {
/**
* Get the underlying search index used by this database.
*
+ *
* @return the search index.
*/
public SongSearchIndex getIndex() {
@@ -91,6 +96,7 @@ public SongSearchIndex getIndex() {
/**
* Register a database listener with this database.
*
+ *
* @param listener the listener.
*/
public void registerDatabaseListener(DatabaseListener listener) {
@@ -113,6 +119,7 @@ public synchronized SongDisplayable[] getSongs() {
/**
* Get all the songs in the database.
*
+ *
* @return an array of all the songs in the database.
*/
public synchronized SongDisplayable[] getSongs(LoadingPane loadingPane) {
@@ -190,9 +197,10 @@ public boolean addSong(final Collection song, final boolean fir
/**
* Add a song to the database.
*
- * @param songs the songs to add.
+ *
+ * @param songs the songs to add.
* @param fireUpdate true if the update should be fired to listeners when
- * adding this song, false otherwise.
+ * adding this song, false otherwise.
* @return true if the operation succeeded, false otherwise.
*/
public synchronized boolean addSong(final SongDisplayable[] songs, final boolean fireUpdate) {
@@ -240,6 +248,7 @@ public synchronized boolean addSong(final SongDisplayable[] songs, final boolean
/**
* Update a song in the database.
*
+ *
* @param song the song to update.
* @return true if the operation succeeded, false otherwise.
*/
@@ -250,9 +259,10 @@ public synchronized boolean updateSong(final SongDisplayable song) {
/**
* Update a song in the database.
*
- * @param song the song to update.
+ *
+ * @param song the song to update.
* @param addIfNotFound true if the song should be added if it's not found,
- * false otherwise.
+ * false otherwise.
* @return true if the operation succeeded, false otherwise.
*/
public synchronized boolean updateSong(final SongDisplayable song, boolean addIfNotFound) {
@@ -302,24 +312,41 @@ public synchronized boolean updateSong(final SongDisplayable song, boolean addIf
/**
* Remove a song from the database.
*
+ *
* @param song the song to remove.
* @return true if the operation succeeded, false otherwise.
*/
public synchronized boolean removeSong(final SongDisplayable song) {
- LOGGER.log(Level.INFO, "Removing song {0}", song.getID());
+ return removeSongs(List.of(song));
+ }
+
+ /**
+ * Remove songs from the database.
+ *
+ *
+ * @param songs the songs to remove.
+ * @return true if the operation succeeded, false otherwise.
+ */
+ public synchronized boolean removeSongs(final List songs) {
+ List ids = songs.stream().map(SongDisplayable::getID).collect(Collectors.toList());
+ LOGGER.log(Level.INFO, "Removing songs {0}", ids);
cacheSongs.clear();
try {
HibernateUtil.execute((Session session) -> {
- Song deletedSong = new SongDao(session).getSongById(song.getID());
- session.delete(deletedSong);
+ for (SongDisplayable song : songs) {
+ Song deletedSong = new SongDao(session).getSongById(song.getID());
+ session.delete(deletedSong);
+ }
});
} catch (IllegalStateException ex) {
- LOGGER.log(Level.WARNING, "Couldn't remove song " + song.getID(), ex);
+ LOGGER.log(Level.WARNING, "Couldn't remove songs " + ids, ex);
return false;
}
- index.remove(song);
+ for (SongDisplayable song : songs) {
+ index.remove(song);
+ }
fireUpdate();
- LOGGER.log(Level.INFO, "Removed song {0}", song.getID());
+ LOGGER.log(Level.INFO, "Removed song {0}", ids);
return true;
}
diff --git a/Quelea/src/main/java/org/quelea/windows/library/LibraryPopupMenu.java b/Quelea/src/main/java/org/quelea/windows/library/LibraryPopupMenu.java
index b3e213e69..8fb22201a 100644
--- a/Quelea/src/main/java/org/quelea/windows/library/LibraryPopupMenu.java
+++ b/Quelea/src/main/java/org/quelea/windows/library/LibraryPopupMenu.java
@@ -1,17 +1,17 @@
-/*
+/*
* This file is part of Quelea, free projection software for churches.
- *
- *
+ *
+ *
* 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 .
*/
@@ -67,29 +67,13 @@ public LibraryPopupMenu() {
exportToPDF = new MenuItem(LabelGrabber.INSTANCE.getLabel("export.pdf.button"), new ImageView(new Image("file:icons/fileexport.png", 16, 16, false, true)));
exportToPDF.setOnAction(new ExportPDFSongActionHandler());
print = new MenuItem(LabelGrabber.INSTANCE.getLabel("library.print.song.text"), new ImageView(new Image("file:icons/fileprint.png", 16, 16, false, true)));
- print.setOnAction(new EventHandler() {
-
- @Override
- public void handle(ActionEvent t) {
- final SongDisplayable song = QueleaApp.get().getMainWindow().getMainPanel().getLibraryPanel().getLibrarySongPanel().getSongList().getSelectedValue();
- if (song != null) {
- if (song.hasChords()) {
- Dialog.buildConfirmation(LabelGrabber.INSTANCE.getLabel("printing.options.text"), LabelGrabber.INSTANCE.getLabel("print.chords.question")).addYesButton(new EventHandler() {
-
- @Override
- public void handle(ActionEvent t) {
- song.setPrintChords(true);
- }
- }).addNoButton(new EventHandler() {
-
- @Override
- public void handle(ActionEvent t) {
- song.setPrintChords(false);
- }
- }).build().showAndWait();
- }
- Printer.getInstance().print(song);
+ print.setOnAction(t -> {
+ final SongDisplayable song = QueleaApp.get().getMainWindow().getMainPanel().getLibraryPanel().getLibrarySongPanel().getSongList().getSelectedValues().get(0);
+ if (song != null) {
+ if (song.hasChords()) {
+ Dialog.buildConfirmation(LabelGrabber.INSTANCE.getLabel("printing.options.text"), LabelGrabber.INSTANCE.getLabel("print.chords.question")).addYesButton(t12 -> song.setPrintChords(true)).addNoButton(t1 -> song.setPrintChords(false)).build().showAndWait();
}
+ Printer.getInstance().print(song);
}
});
@@ -101,4 +85,14 @@ public void handle(ActionEvent t) {
getItems().add(exportToPDF);
getItems().add(print);
}
+
+ public void setMultipleSelected(boolean multipleSelected) {
+ addToSchedule.setDisable(false);
+ copyToSchedule.setDisable(false);
+ preview.setDisable(multipleSelected);
+ editDB.setDisable(multipleSelected);
+ removeFromDB.setDisable(false);
+ exportToPDF.setDisable(multipleSelected);
+ print.setDisable(multipleSelected);
+ }
}
diff --git a/Quelea/src/main/java/org/quelea/windows/library/LibrarySongList.java b/Quelea/src/main/java/org/quelea/windows/library/LibrarySongList.java
index 059c68e2b..6fa6f4136 100644
--- a/Quelea/src/main/java/org/quelea/windows/library/LibrarySongList.java
+++ b/Quelea/src/main/java/org/quelea/windows/library/LibrarySongList.java
@@ -17,6 +17,7 @@
*/
package org.quelea.windows.library;
+import java.util.List;
import java.util.TreeSet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -31,6 +32,7 @@
import javafx.geometry.Pos;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
+import javafx.scene.control.SelectionMode;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.KeyCode;
@@ -60,21 +62,20 @@ public class LibrarySongList extends StackPane {
private static final Logger LOGGER = LoggerUtils.getLogger();
private final LibraryPopupMenu popupMenu;
- private ListView songList;
- private LoadingPane loadingOverlay;
- private LibrarySongPreviewCanvas previewCanvas;
- private AddSongPromptOverlay addSongOverlay;
+ private final ListView songList;
+ private final LoadingPane loadingOverlay;
+ private final LibrarySongPreviewCanvas previewCanvas;
+ private final AddSongPromptOverlay addSongOverlay;
/**
* Create a new library song list.
*
* @param popup true if we want a popup menu to appear on items in this list
- * when right clicked, false if not.
- * @popup true if this list should popup a context menu when right clicked,
- * false otherwise.
+ * when right-clicked, false if not.
*/
public LibrarySongList(boolean popup) {
songList = new ListView<>();
+ songList.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
loadingOverlay = new LoadingPane();
addSongOverlay = new AddSongPromptOverlay();
setAlignment(Pos.CENTER);
@@ -101,7 +102,7 @@ public LibrarySongList(boolean popup) {
}
});
Callback, ListCell> callback = (lv) -> {
- final ListCell cell = new ListCell() {
+ final ListCell cell = new ListCell<>() {
@Override
protected void updateItem(SongDisplayable item, boolean empty) {
super.updateItem(item, empty);
@@ -158,6 +159,7 @@ protected void updateItem(SongDisplayable item, boolean empty) {
}
});
songList.selectionModelProperty().get().selectedItemProperty().addListener((observable, oldSong, song) -> {
+ popupMenu.setMultipleSelected(songList.getSelectionModel().getSelectedIndices().size()>1);
if (previewCanvas != null) {
previewCanvas.setSong(song);
if (song != null && QueleaProperties.get().getShowDBSongPreview()) {
@@ -178,14 +180,8 @@ protected void updateItem(SongDisplayable item, boolean empty) {
if (popup) {
songList.setCellFactory(DisplayableListCell.forListView(popupMenu, callback, null));
}
- new Thread() {
- public void run() {
- refresh();
- }
- }.start();
- SongManager.get().registerDatabaseListener(() -> {
- refresh();
- });
+ new Thread(this::refresh).start();
+ SongManager.get().registerDatabaseListener(this::refresh);
}
private ExecutorService filterService = Executors.newSingleThreadExecutor();
private Future> filterFuture;
@@ -268,12 +264,12 @@ public void filter(final String search) {
}
/**
- * Get the currently selected song.
+ * Get the currently selected songs.
*
- * @return the currently selected song, or null if none is selected.
+ * @return the currently selected song, or an empty list if none is selected.
*/
- public SongDisplayable getSelectedValue() {
- return songList.selectionModelProperty().get().getSelectedItem();
+ public List getSelectedValues() {
+ return songList.selectionModelProperty().get().getSelectedItems();
}
/**
@@ -299,15 +295,16 @@ private void refresh() {
setLoading(true);
});
final ObservableList songs = FXCollections.observableArrayList(SongManager.get().getSongs(loadingOverlay));
- Platform.runLater(new Runnable() {
- @Override
- public void run() {
- songList.itemsProperty().set(songs);
- setLoading(false);
- }
+ Platform.runLater(() -> {
+ songList.itemsProperty().set(songs);
+ setLoading(false);
});
}
+ private boolean hasMultipleSelected() {
+ return songList.getSelectionModel().getSelectedItems().size() > 1;
+ }
+
public void setLoading(boolean loading) {
if (loading) {
loadingOverlay.show();
diff --git a/Quelea/src/main/java/org/quelea/windows/main/actionhandlers/AddSongActionHandler.java b/Quelea/src/main/java/org/quelea/windows/main/actionhandlers/AddSongActionHandler.java
index 5589e89ba..83a7705e5 100644
--- a/Quelea/src/main/java/org/quelea/windows/main/actionhandlers/AddSongActionHandler.java
+++ b/Quelea/src/main/java/org/quelea/windows/main/actionhandlers/AddSongActionHandler.java
@@ -27,6 +27,8 @@
import org.quelea.windows.main.QueleaApp;
import org.quelea.windows.main.schedule.SchedulePanel;
+import java.util.List;
+
/**
* The action listener for adding a song, called when something fires off an
* action that adds a song from the library to the schedule.
@@ -50,21 +52,23 @@ public AddSongActionHandler(boolean updateInDB) {
public void handle(ActionEvent t) {
LibraryPanel libraryPanel = QueleaApp.get().getMainWindow().getMainPanel().getLibraryPanel();
SchedulePanel schedulePanel = QueleaApp.get().getMainWindow().getMainPanel().getSchedulePanel();
- SongDisplayable song = libraryPanel.getLibrarySongPanel().getSongList().getSelectedValue();
- if(QueleaProperties.get().getSongOverflow() || !updateInDB) {
- song = new SongDisplayable(song);
- }
- if(!updateInDB) {
- song.setID(-1);
- song.setNoDBUpdate();
- }
- if(QueleaProperties.get().getUseDefaultTranslation()) {
- String defaultTranslation = QueleaProperties.get().getDefaultTranslationName();
- if(defaultTranslation!=null && !defaultTranslation.trim().isEmpty()) {
- song.setCurrentTranslationLyrics(defaultTranslation);
+ List songs = libraryPanel.getLibrarySongPanel().getSongList().getSelectedValues();
+ for(SongDisplayable song : songs) {
+ if (QueleaProperties.get().getSongOverflow() || !updateInDB) {
+ song = new SongDisplayable(song);
+ }
+ if (!updateInDB) {
+ song.setID(-1);
+ song.setNoDBUpdate();
+ }
+ if (QueleaProperties.get().getUseDefaultTranslation()) {
+ String defaultTranslation = QueleaProperties.get().getDefaultTranslationName();
+ if (defaultTranslation != null && !defaultTranslation.trim().isEmpty()) {
+ song.setCurrentTranslationLyrics(defaultTranslation);
+ }
}
+ schedulePanel.getScheduleList().add(song);
+ libraryPanel.getLibrarySongPanel().getSearchBox().clear();
}
- schedulePanel.getScheduleList().add(song);
- libraryPanel.getLibrarySongPanel().getSearchBox().clear();
}
}
diff --git a/Quelea/src/main/java/org/quelea/windows/main/actionhandlers/EditSongDBActionHandler.java b/Quelea/src/main/java/org/quelea/windows/main/actionhandlers/EditSongDBActionHandler.java
index 929fcb90c..69ef3435d 100644
--- a/Quelea/src/main/java/org/quelea/windows/main/actionhandlers/EditSongDBActionHandler.java
+++ b/Quelea/src/main/java/org/quelea/windows/main/actionhandlers/EditSongDBActionHandler.java
@@ -40,7 +40,7 @@ public class EditSongDBActionHandler implements EventHandler {
public void handle(ActionEvent t) {
Platform.runLater(() -> {
SongEntryWindow songEntryWindow = QueleaApp.get().getMainWindow().getSongEntryWindow();
- SongDisplayable song = QueleaApp.get().getMainWindow().getMainPanel().getLibraryPanel().getLibrarySongPanel().getSongList().getSelectedValue();
+ SongDisplayable song = QueleaApp.get().getMainWindow().getMainPanel().getLibraryPanel().getLibrarySongPanel().getSongList().getSelectedValues().get(0);
if(song != null) {
songEntryWindow.resetEditSong(song);
songEntryWindow.show();
diff --git a/Quelea/src/main/java/org/quelea/windows/main/actionhandlers/PreviewSongActionHandler.java b/Quelea/src/main/java/org/quelea/windows/main/actionhandlers/PreviewSongActionHandler.java
index 46bc520ae..3f55f28b1 100644
--- a/Quelea/src/main/java/org/quelea/windows/main/actionhandlers/PreviewSongActionHandler.java
+++ b/Quelea/src/main/java/org/quelea/windows/main/actionhandlers/PreviewSongActionHandler.java
@@ -35,7 +35,7 @@ public class PreviewSongActionHandler implements EventHandler {
public void handle(ActionEvent t) {
LibraryPanel libraryPanel = QueleaApp.get().getMainWindow().getMainPanel().getLibraryPanel();
PreviewPanel prevPanel = QueleaApp.get().getMainWindow().getMainPanel().getPreviewPanel();
- SongDisplayable song = libraryPanel.getLibrarySongPanel().getSongList().getSelectedValue();
+ SongDisplayable song = libraryPanel.getLibrarySongPanel().getSongList().getSelectedValues().get(0);
prevPanel.setDisplayable(song, 0);
}
diff --git a/Quelea/src/main/java/org/quelea/windows/main/actionhandlers/RemoveSongDBActionHandler.java b/Quelea/src/main/java/org/quelea/windows/main/actionhandlers/RemoveSongDBActionHandler.java
index 9f4467271..f0fb096af 100644
--- a/Quelea/src/main/java/org/quelea/windows/main/actionhandlers/RemoveSongDBActionHandler.java
+++ b/Quelea/src/main/java/org/quelea/windows/main/actionhandlers/RemoveSongDBActionHandler.java
@@ -1,23 +1,24 @@
-/*
+/*
* This file is part of Quelea, free projection software for churches.
- *
- *
+ *
+ *
* 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 org.quelea.windows.main.actionhandlers;
import javafx.application.Platform;
+import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import org.javafx.dialog.Dialog;
@@ -28,9 +29,12 @@
import org.quelea.windows.main.MainWindow;
import org.quelea.windows.main.QueleaApp;
+import java.util.List;
+
/**
* Action listener that removes the selected song from the database.
*
+ *
* @author Michael
*/
public class RemoveSongDBActionHandler implements EventHandler {
@@ -40,59 +44,45 @@ public class RemoveSongDBActionHandler implements EventHandler {
/**
* Remove the selected song from the database.
*
+ *
* @param t the action event.
*/
@Override
public void handle(ActionEvent t) {
MainWindow mainWindow = QueleaApp.get().getMainWindow();
final LibrarySongList songList = mainWindow.getMainPanel().getLibraryPanel().getLibrarySongPanel().getSongList();
- int index = songList.getListView().getSelectionModel().getSelectedIndex();
- if(index == -1) {
+ List indices = songList.getListView().getSelectionModel().getSelectedIndices();
+ if (indices.isEmpty()) {
return;
}
- final SongDisplayable song = songList.getListView().itemsProperty().get().get(index);
- if(song == null) {
- return;
+
+ String confirmRemoveQuestion;
+ if (indices.size() == 1) {
+ final SongDisplayable song = songList.getListView().itemsProperty().get().get(indices.get(0));
+ confirmRemoveQuestion = LabelGrabber.INSTANCE.getLabel("confirm.remove.question").replace("$1", song.getTitle());
+ } else {
+ confirmRemoveQuestion = LabelGrabber.INSTANCE.getLabel("confirm.remove.bulk.question").replace("$1", Integer.toString(indices.size()));
}
+
yes = false;
Dialog.buildConfirmation(LabelGrabber.INSTANCE.getLabel("confirm.remove.text"),
- LabelGrabber.INSTANCE.getLabel("confirm.remove.question").replace("$1", song.getTitle()))
- .addYesButton(new EventHandler() {
- @Override
- public void handle(ActionEvent t) {
- yes = true;
- }
- }).addNoButton(new EventHandler() {
- @Override
- public void handle(ActionEvent t) {
- }
- }).build().showAndWait();
- if(yes) {
+ confirmRemoveQuestion)
+ .addYesButton(t1 -> yes = true).addNoButton(t12 -> {
+ }).build().showAndWait();
+ if (yes) {
songList.setLoading(true);
- new Thread() {
- @Override
- public void run() {
- if(!SongManager.get().removeSong(song)) {
- Platform.runLater(new Runnable() {
- @Override
- public void run() {
- Dialog.showError(LabelGrabber.INSTANCE.getLabel("error.text"), LabelGrabber.INSTANCE.getLabel("error.removing.song.db"));
- }
- });
- }
- else {
- song.setID(-1);
- Platform.runLater(new Runnable() {
-
- @Override
- public void run() {
- songList.getListView().itemsProperty().get().remove(song);
- songList.setLoading(false);
- }
- });
- }
+ new Thread(() -> {
+ ObservableList items = songList.getListView().getSelectionModel().getSelectedItems();
+ if (!SongManager.get().removeSongs(items)) {
+ Platform.runLater(() -> Dialog.showError(LabelGrabber.INSTANCE.getLabel("error.text"), LabelGrabber.INSTANCE.getLabel("error.removing.song.db")));
+ } else {
+ items.forEach(s -> s.setID(-1));
}
- }.start();
+ Platform.runLater(() -> {
+ songList.getListView().itemsProperty().get().removeAll(items);
+ songList.setLoading(false);
+ });
+ }).start();
}
}
}