2016-06-13 3 views
0

Я пытаюсь создать java-медиаплеер с DLNA Control Point.javafx TableView с динамическим ContextMenu в строках

Существует таблица со мультимедийными файлами.
С помощью JavaFX TableView, что я узнал, в обратном вызове setRowFactory мы можем добавлять слушателей к событиям, генерируемым свойствами элементов таблицы. Все типы событий TableView запускаются только во внутренних изменениях данных таблицы. Я не могу найти способ добраться до строк таблицы в случае какого-либо внешнего события или логики и изменить, например, ContextMenu для каждой строки.

Каждая строка в таблице представляет собой мультимедийный файл. ContextMenu изначально имеет только пункты меню «Воспроизвести» (локально) и «Удалить». Например, в сети появилось устройство визуализации DLNA. DLN-поток обнаружит событие, и я хочу добавить пункт меню «Воспроизвести это устройство» в контекстное меню каждой строки таблицы. Соответственно, мне нужно будет удалить этот элемент, как только соответствующее устройство погаснет.

Как подключиться к ContextMenu каждой строки извне rowFactory?

Вот код таблицы и строки завода

public FileManager(GuiController guiController) { 

     gCtrl = guiController; 
     gCtrl.fileName.setCellValueFactory(new PropertyValueFactory<FileTableItem, String>("Name")); 
     gCtrl.fileType.setCellValueFactory(new PropertyValueFactory<FileTableItem, String>("Type")); 
     gCtrl.fileSize.setCellValueFactory(new PropertyValueFactory<FileTableItem, String>("Size")); 
     gCtrl.fileTime.setCellValueFactory(new PropertyValueFactory<FileTableItem, String>("modifiedTime")); 


     gCtrl.filesTable.setRowFactory(tv -> { 
      TableRow<FileTableItem> row = new TableRow<>(); 
      row.emptyProperty().addListener((obs, wasEmpty, isEmpty) -> { 
       if (!isEmpty) { 
        FileTableItem file = row.getItem(); 
        ContextMenu contextMenu = new ContextMenu(); 

        if (file.isPlayable()) { 
         row.setOnMouseClicked(event -> { 
          if (event.getClickCount() == 2) { 
           gCtrl.playMedia(file.getAbsolutePath()); 
          } 
         }); 

         MenuItem playMenuItem = new MenuItem("Play"); 
         playMenuItem.setOnAction(event -> { 
          gCtrl.playMedia(file.getAbsolutePath()); 
         }); 
         contextMenu.getItems().add(playMenuItem); 
        } 

        if (file.canWrite()) { 
         MenuItem deleteMenuItem = new MenuItem("Delete"); 
         deleteMenuItem.setOnAction(event -> { 
          row.getItem().delete(); 
         }); 
         contextMenu.getItems().add(deleteMenuItem); 
        } 
        row.setContextMenu(contextMenu); 
       } 
      }); 
      return row; 
     }); 
     gCtrl.filesTable.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); 
    } 
    ... 
    public class FileTableItem extends File { 
    ... 
    } 

Заранее спасибо

+1

Один из методов заключается в том, чтобы прикрепить к 'TableView' onContextMenuRequested' и изменить контекстное меню в соответствии с текущей выбранной строкой. Другой подход состоял бы в том, чтобы иметь все возможные пункты меню и привязывать каждое свойство 'disable' или' visible' к соответствующему выражению на основе выбранного элемента. – Itai

+0

не обязательно добавлять все возможные пункты меню, например, мы не можем знать, какие DLNA-устройства появятся в сети. «onContextMenuRequested» - это интересный подход - таким образом мы можем обновлять ContextMenu каждый раз, когда он вызывается. Я боюсь, это может быть менее эффективным и более трудоемким, чем фоновое обновление на внешнем событии. Но я попробую. Какое прослушивание событий можно подключить к нему? – Liphtier

+0

ОК, я получил его работу с row.setOnContextMenuRequested - большое спасибо! Но мне все еще интересно - это единственный возможный способ? Очень странно, если мы не можем напрямую обращаться к строкам TableView с помощью getRows – Liphtier

ответ

2

JavaFX в целом соответствует модели типа MVC/MVP. В представлении таблицы TableRow является частью представления: поэтому для изменения внешнего вида строки таблицы (включая содержимое контекстного меню, связанного с ней в этом случае), вы должны позволить ей наблюдать какую-то модель и измените то, что отображается в контекстном меню, которое вы меняете на эту модель.

Я не совсем уверен, правильно ли понял ваш случай использования, но я думаю, что я понимаю, что каждый элемент в таблице может иметь другой набор устройств, связанных с ним. Таким образом, вы бы иметь объект класса ищет что-то вроде этого:

public class FileTableItem extends File { 

    private final ObservableList<Device> devices = FXCollections.observableArrayList(); 

    public ObservableList<Device> getDevices() { 
     return devices ; 
    } 
} 

При создании строки таблицы, вам это нужно, чтобы наблюдать список устройств, связанных с его текущим элементом; вы можете сделать это с помощью ListChangeListener. Конечно, элемент, отображаемый в любое заданное время по строке, может меняться в произвольные моменты времени, находящиеся за пределами вашего контроля (например, когда пользователь прокручивает таблицу), поэтому вам нужно наблюдать свойство элемента строки и следить за тем, чтобы наблюдатель ListChangeListener правильный список элементов. Вот код, который достигает этого:

TableView<FileTableItem> filesTable = new TableView<>(); 
filesTable.setRowFactory(tv -> { 
    TableRow<FileTableItem> row = new TableRow<>(); 
    ContextMenu menu = new ContextMenu(); 
    ListChangeListener<FileTableItem> changeListener = (ListChangeListener.Change<? extends FileTableItem> c) -> 
     updateMenu(menu, row.getItem().getDevices()); 

    row.itemProperty().addListener((obs, oldItem, newItem) -> { 
     if (oldItem != null) { 
      oldItem.getDevices().removeListener(changeListener); 
     } 
     if (newItem == null) { 
      contextMenu.getItems().clear(); 
     } else { 
      newItem.getDevices().addListener(changeListener); 
      updateMenu(menu, newItem.getDevices()); 
     } 
    }); 

    row.emptyProperty().addListener((obs, wasEmpty, isNowEmpty) -> 
     row.setContextMenu(isNowEmpty ? null : menu)); 

    return row ; 
}); 

// ... 

private void updateMenu(ContextMenu menu, List<Device> devices) { 
    menu.getItems().clear(); 
    for (Device device : devices) { 
     MenuItem item = new MenuItem(device.toString()); 
     item.setOnAction(e -> { /* ... */ }); 
     menu.getItems().add(item); 
    } 

} 

Это автоматически обновит контекстное меню, если список устройств изменится.

В комментариях ниже вашего вопроса вы сказали, что хотите, чтобы в таблице был метод getRows(). Такой метод не существует, отчасти потому, что дизайн использует подход MVC, как описано. Даже если бы это было так, это не помогло бы: предположим, что список устройств для элемента, прокрученного вне поля зрения, изменился - в этом случае не будет TableRow, соответствующего этому элементу, поэтому вы не сможете получить ссылку для изменения его контекстного меню. Вместо этого, с описанной настройкой, вы просто обновите модель в точке кода, где вы собираетесь обновлять строку таблицы.

Возможно, вам потребуется изменить это, если у вас есть пункты меню, которые не зависят от списка и т. Д., Но этого должно быть достаточно, чтобы показать эту идею.

Это SSCCE. В этом примере в таблице изначально 20 элементов, без подключенных устройств. В контекстном меню для каждого отображается только опция «Удалить», которая удаляет элемент. Вместо фоновой задачи, которая обновляет элементы, я имитировал это с помощью некоторых элементов управления. Вы можете выбрать элемент в таблице и добавить к нему устройства, нажав кнопку «Добавить устройство»: впоследствии вы увидите «Воспроизвести на устройстве ....» в своем контекстном меню. Аналогично «Удалить устройство» удалит последнее устройство в списке. Флажок «Задержка» задерживает добавление или удаление устройства на две секунды: это позволяет вам нажимать кнопку, а затем (быстро) открывать контекстное меню; вы увидите обновление контекстного меню во время его показа.

import javafx.animation.PauseTransition; 
import javafx.application.Application; 
import javafx.beans.property.SimpleStringProperty; 
import javafx.collections.FXCollections; 
import javafx.collections.ListChangeListener; 
import javafx.collections.ObservableList; 
import javafx.geometry.Insets; 
import javafx.scene.Scene; 
import javafx.scene.control.Button; 
import javafx.scene.control.CheckBox; 
import javafx.scene.control.ContextMenu; 
import javafx.scene.control.MenuItem; 
import javafx.scene.control.TableColumn; 
import javafx.scene.control.TableRow; 
import javafx.scene.control.TableView; 
import javafx.scene.layout.BorderPane; 
import javafx.scene.layout.HBox; 
import javafx.stage.Stage; 
import javafx.util.Duration; 

public class DynamicContextMenuInTable extends Application { 

    private int deviceCount = 0 ; 

    private void addDeviceToItem(Item item) { 
     Device newDevice = new Device("Device "+(++deviceCount)); 
     item.getDevices().add(newDevice); 
    } 

    private void removeDeviceFromItem(Item item) { 
     if (! item.getDevices().isEmpty()) { 
      item.getDevices().remove(item.getDevices().size() - 1); 
     } 
    } 

    @Override 
    public void start(Stage primaryStage) { 
     TableView<Item> table = new TableView<>(); 
     TableColumn<Item, String> itemCol = new TableColumn<>("Item"); 
     itemCol.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getName())); 
     table.getColumns().add(itemCol); 

     table.setRowFactory(tv -> { 
      TableRow<Item> row = new TableRow<>(); 
      ContextMenu menu = new ContextMenu(); 

      MenuItem delete = new MenuItem("Delete"); 
      delete.setOnAction(e -> table.getItems().remove(row.getItem())); 

      menu.getItems().add(delete); 

      ListChangeListener<Device> deviceListListener = c -> 
       updateContextMenu(row.getItem(), menu); 

      row.itemProperty().addListener((obs, oldItem, newItem) -> { 
       if (oldItem != null) { 
        oldItem.getDevices().removeListener(deviceListListener); 
       } 
       if (newItem != null) { 
        newItem.getDevices().addListener(deviceListListener); 
        updateContextMenu(row.getItem(), menu); 
       } 
      }); 

      row.emptyProperty().addListener((obs, wasEmpty, isNowEmpty) -> 
       row.setContextMenu(isNowEmpty ? null : menu)); 

      return row ; 
     }); 

     CheckBox delay = new CheckBox("Delay"); 

     Button addDeviceButton = new Button("Add device"); 
     addDeviceButton.disableProperty().bind(table.getSelectionModel().selectedItemProperty().isNull()); 
     addDeviceButton.setOnAction(e -> { 
      Item selectedItem = table.getSelectionModel().getSelectedItem(); 
      if (delay.isSelected()) { 
       PauseTransition pause = new PauseTransition(Duration.seconds(2)); 
       pause.setOnFinished(evt -> { 
        addDeviceToItem(selectedItem); 
       }); 
       pause.play(); 
      } else { 
       addDeviceToItem(selectedItem); 
      } 
     }); 

     Button removeDeviceButton = new Button("Remove device"); 
     removeDeviceButton.disableProperty().bind(table.getSelectionModel().selectedItemProperty().isNull()); 
     removeDeviceButton.setOnAction(e -> { 
      Item selectedItem = table.getSelectionModel().getSelectedItem() ; 
      if (delay.isSelected()) { 
       PauseTransition pause = new PauseTransition(Duration.seconds(2)); 
       pause.setOnFinished(evt -> removeDeviceFromItem(selectedItem)); 
       pause.play(); 
      } else { 
       removeDeviceFromItem(selectedItem); 
      } 
     }); 

     HBox buttons = new HBox(5, addDeviceButton, removeDeviceButton, delay); 
     BorderPane.setMargin(buttons, new Insets(5)); 
     BorderPane root = new BorderPane(table, buttons, null, null, null); 

     for (int i = 1 ; i <= 20; i++) { 
      table.getItems().add(new Item("Item "+i)); 
     } 

     Scene scene = new Scene(root); 
     primaryStage.setScene(scene); 
     primaryStage.show(); 
    } 

    private void updateContextMenu(Item item, ContextMenu menu) { 
     if (menu.getItems().size() > 1) { 
      menu.getItems().subList(1, menu.getItems().size()).clear(); 
     } 
     for (Device device : item.getDevices()) { 
      MenuItem menuItem = new MenuItem("Play on "+device.getName()); 
      menuItem.setOnAction(e -> System.out.println("Play "+item.getName()+" on "+device.getName())); 
      menu.getItems().add(menuItem); 
     } 
    } 

    public static class Device { 
     private final String name ; 

     public Device(String name) { 
      this.name = name ; 
     } 

     public String getName() { 
      return name ; 
     } 

     @Override 
     public String toString() { 
      return getName(); 
     } 
    } 

    public static class Item { 
     private final ObservableList<Device> devices = FXCollections.observableArrayList() ; 

     private final String name ; 

     public Item(String name) { 
      this.name = name ; 
     } 

     public ObservableList<Device> getDevices() { 
      return devices ; 
     } 

     public String getName() { 
      return name ; 
     } 
    } 

    public static void main(String[] args) { 
     launch(args); 
    } 
} 

enter image description here

enter image description here

enter image description here

+0

Здесь есть две проблемы: 1. Список DLNA-устройств - это ConurrentHashMap и не является частью FileTableItem и GUI. Он поддерживается другим модулем, который не может зависеть от FXCollections.observableArrayList. 2. Непонятно, что вы здесь делаете: Даже если я упакую список внешних устройств локально в наблюдаемый список, row.itemProperty(). AddListener будет захватывать события и обновлять меню только при изменении данных таблицы, например. об изменении файлов, а не в устройствах! Это проблема. И ListChangeListener, кажется, ничего не делает, по крайней мере, так, как я пытался его использовать. – Liphtier

+1

Обновлено с некоторыми разъяснениями о слушателях и о том, как реализовать этот подход. Обратите внимание, что свойство 'item' изменяется не только при изменении модели данных, но и при повторном использовании строки для отображения нового элемента, например, когда пользователь прокручивает таблицу. «ListChangeListener» наблюдает за списком устройств; слушатель свойства item просто гарантирует, что он наблюдает * правильный * список устройств. –

+0

@ Liphtier Добавлена ​​демонстрация. Надеюсь, что это станет более ясным. –

0

С советом от sillyfly я получил рабочий раствор, однако он потенциально может иметь недостатки производительности. Поэтому было бы интересно найти лучшую.

class FileManager { 


    private GuiController gCtrl; 

    protected Menu playToSub = new Menu("Play to..."); 
    Map<String, MenuItem> playToItems = new HashMap<String, MenuItem>(); 

    public FileManager(GuiController guiController) { 

     gCtrl = guiController; 

     gCtrl.fileName.setCellValueFactory(new PropertyValueFactory<FileTableItem, String>("Name")); 
     gCtrl.fileType.setCellValueFactory(new PropertyValueFactory<FileTableItem, String>("Type")); 
     gCtrl.fileSize.setCellValueFactory(new PropertyValueFactory<FileTableItem, String>("Size")); 
     gCtrl.fileTime.setCellValueFactory(new PropertyValueFactory<FileTableItem, String>("modifiedTime")); 

     gCtrl.filesTable.setRowFactory(tv -> { 
      TableRow<FileTableItem> row = new TableRow<>(); 
      row.emptyProperty().addListener((obs, wasEmpty, isEmpty) -> { 
       if (!isEmpty) { 
        FileTableItem file = row.getItem(); 
        ContextMenu contextMenu = new ContextMenu(); 

        if (file.isPlayable()) { 
         row.setOnMouseClicked(event -> { 
          if (event.getClickCount() == 2) { 
           gCtrl.mainApp.playFile = file.getName(); 
           gCtrl.playMedia(file.getAbsolutePath()); 
          } 
         }); 

         MenuItem playMenuItem = new MenuItem("Play"); 
         playMenuItem.setOnAction(event -> { 
          gCtrl.mainApp.playFile = file.getName(); 
          gCtrl.playMedia(file.getAbsolutePath()); 
         }); 
         contextMenu.getItems().add(playMenuItem); 
        } 

        if (file.canWrite()) { 
         MenuItem deleteMenuItem = new MenuItem("Delete"); 
         deleteMenuItem.setOnAction(event -> { 
          row.getItem().delete(); 
         }); 
         contextMenu.getItems().add(deleteMenuItem); 
        } 
        row.setContextMenu(contextMenu); 
       } 
      }); 
      row.setOnContextMenuRequested((event) -> { 

       /// Here, just before showing the context menu we can decide what to show in it 
       /// In this particular case it's OK, but it may be time expensive in general 
       if(! row.isEmpty()) { 
        if(gCtrl.mainApp.playDevices.size() > 0) { 
         if(! row.getContextMenu().getItems().contains(playToSub)) { 
          row.getContextMenu().getItems().add(1, playToSub); 
         } 
        } 
        else { 
         if(row.getContextMenu().getItems().contains(playToSub)) { 
          row.getContextMenu().getItems().remove(playToSub); 
         } 
        } 
       } 
      }); 
      return row; 
     }); 
     gCtrl.filesTable.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); 
    } 

    /// addPlayToMenuItem and removePlayToMenuItem are run from Gui Controller 
    /// which in turn is notified by events in UPNP module 
    /// The playTo sub menu items are managed here 
    public void addPlayToMenuItem(String uuid, String name, URL iconUrl) { 
     MenuItem playToItem = new PlayToMenuItem(uuid, name, iconUrl); 
     playToItems.put(uuid, playToItem); 
     playToSub.getItems().add(playToItem); 
    } 

    public void removePlayToMenuItem(String uuid) { 
     if(playToItems.containsKey(uuid)) { 
      playToSub.getItems().remove(playToItems.get(uuid)); 
      playToItems.remove(uuid); 
     } 
    } 

    public class PlayToMenuItem extends MenuItem { 
     PlayToMenuItem(String uuid, String name, URL iconUrl) { 
      super(); 
      if (iconUrl != null) { 
       Image icon = new Image(iconUrl.toString()); 
       ImageView imgView = new ImageView(icon); 
       imgView.setFitWidth(12); 
       imgView.setPreserveRatio(true); 
       imgView.setSmooth(true); 
       imgView.setCache(true); 
       setGraphic(imgView); 
      } 
      setText(name); 
      setOnAction(event -> { 
       gCtrl.mainApp.playFile = gCtrl.filesTable.getSelectionModel().getSelectedItem().getName(); 
       gCtrl.mainApp.startRemotePlay(uuid); 
      }); 
     } 
    } 

    /// Other class methods and members 

} 
Смежные вопросы