1919import javafx .scene .input .Dragboard ;
2020import javafx .scene .input .TransferMode ;
2121import javafx .scene .layout .GridPane ;
22+ import javafx .scene .layout .Priority ;
23+ import javafx .scene .layout .RowConstraints ;
2224import javafx .scene .media .MediaPlayer ;
2325import javafx .stage .FileChooser ;
2426import javafx .stage .Stage ;
3840
3941public final class MainWindowController implements IAudioTimer , TrayIconListener {
4042
43+ @ FXML
44+ private GridPane grpCustom ;
45+ @ FXML
46+ private TitledPane pneCustom ;
4147 @ FXML
4248 private TitledPane pneOther ;
4349 @ FXML
@@ -269,8 +275,8 @@ private List<SoundPane> getSoundPanes(final GridPane parent) {
269275 private void openFile (final String path ) {
270276 if (path == null )
271277 throw new NullPointerException ("Path cannot be null!" );
272- if (path .isEmpty ())
273- throw new IllegalArgumentException ("Path cannot be empty !" );
278+ if (path .isBlank ())
279+ throw new IllegalArgumentException ("Path cannot be blank !" );
274280
275281 try {
276282 helpUtils .openFile (new RunnableFileOpener (path , new IRunnableHelper () {
@@ -319,6 +325,19 @@ private void initialize() {
319325 e .setVisible (true );
320326 e .setManaged (true );
321327 });
328+
329+ final List <SoundPane > customSoundPanes = getSoundPanes (grpCustom );
330+ if (!customSoundPanes .isEmpty ()) {
331+ pneCustom .setExpanded (true );
332+ pneCustom .setVisible (true );
333+ pneCustom .setManaged (true );
334+
335+ customSoundPanes .forEach (e -> {
336+ e .setVisible (true );
337+ e .setManaged (true );
338+ });
339+ }
340+
322341 btnClearSearch .setVisible (false );
323342 btnClearSearch .setManaged (false );
324343
@@ -344,15 +363,36 @@ private void initialize() {
344363 });
345364 return ;
346365 }
366+
347367 Platform .runLater (() -> {
348368 getAllSoundPanes ()
349369 .forEach (e -> {
350370 e .setVisible (e .getName ().toLowerCase ().contains (newValue .trim ().toLowerCase ()));
351371 e .setManaged (e .isVisible ());
352372 });
353373
374+ getSoundPanes (grpCustom )
375+ .forEach (e -> {
376+ e .setVisible (e .getName ().toLowerCase ().contains (newValue .trim ().toLowerCase ()));
377+ e .setManaged (e .isVisible ());
378+ });
379+
380+ // Check if there are still active sound panes on pneCustom
381+ final List <SoundPane > customSoundPanes = getSoundPanes (grpCustom );
382+ if (!customSoundPanes .isEmpty ()) {
383+ customSoundPanes .stream ().filter (Node ::isVisible ).findFirst ().ifPresentOrElse (_ -> {
384+ pneCustom .setExpanded (true );
385+ pneCustom .setVisible (true );
386+ pneCustom .setManaged (true );
387+ }, () -> {
388+ pneCustom .setExpanded (false );
389+ pneCustom .setVisible (false );
390+ pneCustom .setManaged (false );
391+ });
392+ }
393+
354394 // Check if there are still active sound panes on pneNature
355- getSoundPanes (grpNature ).stream ().filter (Node ::isVisible ).findFirst ().ifPresentOrElse (e -> {
395+ getSoundPanes (grpNature ).stream ().filter (Node ::isVisible ).findFirst ().ifPresentOrElse (_ -> {
356396 pneNature .setExpanded (true );
357397 pneNature .setVisible (true );
358398 pneNature .setManaged (true );
@@ -363,7 +403,7 @@ private void initialize() {
363403 });
364404
365405 // Check if there are still active sound panes on pneOffice
366- getSoundPanes (grpOffice ).stream ().filter (Node ::isVisible ).findFirst ().ifPresentOrElse (e -> {
406+ getSoundPanes (grpOffice ).stream ().filter (Node ::isVisible ).findFirst ().ifPresentOrElse (_ -> {
367407 pneOffice .setExpanded (true );
368408 pneOffice .setVisible (true );
369409 pneOffice .setManaged (true );
@@ -374,7 +414,7 @@ private void initialize() {
374414 });
375415
376416 // Check if there are still active sound panes on pneAudiences
377- getSoundPanes (grpAudiences ).stream ().filter (Node ::isVisible ).findFirst ().ifPresentOrElse (e -> {
417+ getSoundPanes (grpAudiences ).stream ().filter (Node ::isVisible ).findFirst ().ifPresentOrElse (_ -> {
378418 pneAudiences .setExpanded (true );
379419 pneAudiences .setVisible (true );
380420 pneAudiences .setManaged (true );
@@ -385,7 +425,7 @@ private void initialize() {
385425 });
386426
387427 // Check if there are still active sound panes on pneRadioFrequencyStatic
388- getSoundPanes (grpRadioFrequencyStatic ).stream ().filter (Node ::isVisible ).findFirst ().ifPresentOrElse (e -> {
428+ getSoundPanes (grpRadioFrequencyStatic ).stream ().filter (Node ::isVisible ).findFirst ().ifPresentOrElse (_ -> {
389429 pneRadioFrequencyStatic .setExpanded (true );
390430 pneRadioFrequencyStatic .setVisible (true );
391431 pneRadioFrequencyStatic .setManaged (true );
@@ -396,7 +436,7 @@ private void initialize() {
396436 });
397437
398438 // Check if there are still active sound panes on pneOther
399- getSoundPanes (grpOther ).stream ().filter (Node ::isVisible ).findFirst ().ifPresentOrElse (e -> {
439+ getSoundPanes (grpOther ).stream ().filter (Node ::isVisible ).findFirst ().ifPresentOrElse (_ -> {
400440 pneOther .setExpanded (true );
401441 pneOther .setVisible (true );
402442 pneOther .setManaged (true );
@@ -442,6 +482,52 @@ private void openSoundPresetAction() {
442482 }
443483 }
444484
485+ /**
486+ * Open a custom sound
487+ */
488+ @ FXML
489+ private void addCustomSound () {
490+ logger .info ("Opening a custom sound" );
491+ final FileChooser chooser = new FileChooser ();
492+
493+ final FileChooser .ExtensionFilter extFilter = new FileChooser .ExtensionFilter ("MP3 (*.mp3)" , "*.mp3" );
494+ chooser .getExtensionFilters ().add (extFilter );
495+
496+ final File file = chooser .showOpenDialog (new Stage ());
497+
498+ if (file != null && file .exists ()) {
499+ if (file .getAbsolutePath ().isBlank ())
500+ throw new IllegalArgumentException ("Path cannot be blank!" );
501+
502+ logger .info ("Loading custom sound from {}" , file .getAbsolutePath ());
503+
504+ try {
505+ final RowConstraints row = new RowConstraints ();
506+ row .setVgrow (Priority .ALWAYS );
507+
508+ grpCustom .getRowConstraints ().add (row );
509+
510+ final SoundPane customSoundPane = new SoundPane ();
511+ final int count = getSoundPanes (grpCustom ).size () + 1 ;
512+
513+ customSoundPane .setName (translationBundle .getString ("CustomSound" ) + " #" + count );
514+ customSoundPane .setMediaKey ("custom" + count );
515+ customSoundPane .setMediaPath (file .toURI ().toURL ().toString ());
516+ customSoundPane .setImage ("/images/customsound.png" );
517+ customSoundPane .setResourceFile (false );
518+
519+ grpCustom .add (customSoundPane , 0 , count - 1 );
520+
521+ pneCustom .setExpanded (true );
522+ pneCustom .setVisible (true );
523+ pneCustom .setManaged (true );
524+ } catch (final IOException ex ) {
525+ logger .error ("Unable to open the custom sound from {}" , file .getAbsolutePath (), ex );
526+ FxUtils .showErrorAlert (translationBundle .getString ("OpenCustomSoundError" ), ex .toString (), getClass ().getResourceAsStream (SharedVariables .ICON_URL ));
527+ }
528+ }
529+ }
530+
445531 /**
446532 * Open a sound preset
447533 *
@@ -450,17 +536,17 @@ private void openSoundPresetAction() {
450536 private void openSoundPreset (final String path ) {
451537 if (path == null )
452538 throw new NullPointerException ("Path cannot be null!" );
453- if (path .isEmpty ())
454- throw new IllegalArgumentException ("Path cannot be empty !" );
539+ if (path .isBlank ())
540+ throw new IllegalArgumentException ("Path cannot be blank !" );
455541
456542 logger .info ("Loading sound preset from {}" , path );
457543
458544 try {
459545 final Path filePath = Path .of (path );
460546 final String actual = Files .readString (filePath );
461547
462- if (actual .isEmpty ())
463- throw new IllegalArgumentException ("Sound preset cannot be empty !" );
548+ if (actual .isBlank ())
549+ throw new IllegalArgumentException ("Sound preset cannot be blank !" );
464550
465551 final TypeReference <HashMap <String , Double >> typeRef = new TypeReference <>() {
466552 };
@@ -518,6 +604,10 @@ private void playPauseAction() {
518604 for (final SoundPane soundPane : getAllSoundPanes ()) {
519605 soundPane .playPause ();
520606 }
607+
608+ for (final SoundPane soundPane : getSoundPanes (grpCustom )) {
609+ soundPane .playPause ();
610+ }
521611 } catch (final MediaPlayerException ex ) {
522612 logger .error ("Unable to play / pause MediaPlayer" , ex );
523613 FxUtils .showErrorAlert (translationBundle .getString ("PlayPauseError" ), ex .toString (), getClass ().getResourceAsStream (SharedVariables .ICON_URL ));
@@ -531,6 +621,13 @@ private void playPauseAction() {
531621 private void resetAction () {
532622 logger .info ("Resetting all audio sliders" );
533623 getAllSoundPanes ().forEach (e -> e .getSlider ().setValue (0 ));
624+
625+ getSoundPanes (grpCustom ).forEach (SoundPane ::disposeMediaPlayer );
626+ grpCustom .getChildren ().clear ();
627+
628+ pneCustom .setExpanded (false );
629+ pneCustom .setVisible (false );
630+ pneCustom .setManaged (false );
534631 }
535632
536633 /**
@@ -684,6 +781,7 @@ private void clearSearchAction() {
684781 public void fired () {
685782 cancelTimer ();
686783 getAllSoundPanes ().forEach (SoundPane ::pause );
784+ getSoundPanes (grpCustom ).forEach (SoundPane ::pause );
687785
688786 if (Boolean .parseBoolean (settingsController .getProperties ().getProperty ("timerComputerShutdown" , "false" ))) {
689787 final String command = switch (platformName .toLowerCase ()) {
0 commit comments