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(); } } }