diff --git a/src/main/java/io/github/dsheirer/gui/JavaFxWindowManager.java b/src/main/java/io/github/dsheirer/gui/JavaFxWindowManager.java index 71108b94a..987e67c27 100644 --- a/src/main/java/io/github/dsheirer/gui/JavaFxWindowManager.java +++ b/src/main/java/io/github/dsheirer/gui/JavaFxWindowManager.java @@ -24,8 +24,6 @@ import io.github.dsheirer.controller.channel.map.ChannelMap; import io.github.dsheirer.controller.channel.map.ChannelRange; import io.github.dsheirer.eventbus.MyEventBus; -import io.github.dsheirer.gui.dmr.DMRRecordingViewer; -import io.github.dsheirer.gui.dmr.ViewDmrRecordingViewerRequest; import io.github.dsheirer.gui.icon.IconManager; import io.github.dsheirer.gui.icon.ViewIconManagerRequest; import io.github.dsheirer.gui.playlist.PlaylistEditor; @@ -37,6 +35,8 @@ import io.github.dsheirer.gui.preference.UserPreferencesEditor; import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest; import io.github.dsheirer.gui.preference.calibration.CalibrationDialog; +import io.github.dsheirer.gui.viewer.RecordingViewer; +import io.github.dsheirer.gui.viewer.ViewRecordingViewerRequest; import io.github.dsheirer.icon.IconModel; import io.github.dsheirer.jmbe.JmbeEditor; import io.github.dsheirer.jmbe.JmbeEditorRequest; @@ -67,7 +67,7 @@ public class JavaFxWindowManager extends Application public static final String USER_PREFERENCES_EDITOR = "preferences"; public static final String STAGE_MONITOR_KEY_CALIBRATION_DIALOG = "calibration.dialog"; public static final String STAGE_MONITOR_KEY_CHANNEL_MAP_EDITOR = "channel.map"; - public static final String STAGE_MONITOR_KEY_DMR_MESSAGE_VIEWER = "dmr.message.viewer"; + public static final String STAGE_MONITOR_KEY_RECORDING_VIEWER = "recording.viewer"; public static final String STAGE_MONITOR_KEY_ICON_MANAGER_EDITOR = "icon.manager"; public static final String STAGE_MONITOR_KEY_JMBE_EDITOR = "jmbe.editor"; public static final String STAGE_MONITOR_KEY_PLAYLIST_EDITOR = "playlist"; @@ -82,7 +82,7 @@ public class JavaFxWindowManager extends Application private TunerManager mTunerManager; private UserPreferences mUserPreferences; private UserPreferencesEditor mUserPreferencesEditor; - private DMRRecordingViewer mDmrRecordingViewer; + private RecordingViewer mRecordingViewer; private Stage mChannelMapStage; private Stage mIconManagerStage; @@ -195,31 +195,31 @@ public CalibrationDialog getCalibrationDialog(UserPreferences userPreferences) } /** - * Stage for the DMR Message Viewer + * Stage for the recording viewer */ - public Stage getDmrRecordingViewerStage() + public Stage getRecordingViewerStage() { if(mDmrRecordingViewerStage == null) { createJFXPanel(); - Scene scene = new Scene(getDmrRecordingViewer(), 1100, 800); + Scene scene = new Scene(getRecordingViewer(), 1100, 800); mDmrRecordingViewerStage = new Stage(); mDmrRecordingViewerStage.setTitle("sdrtrunk - DMR Recording Viewer"); mDmrRecordingViewerStage.setScene(scene); - mUserPreferences.getJavaFxPreferences().monitor(mDmrRecordingViewerStage, STAGE_MONITOR_KEY_DMR_MESSAGE_VIEWER); + mUserPreferences.getJavaFxPreferences().monitor(mDmrRecordingViewerStage, STAGE_MONITOR_KEY_RECORDING_VIEWER); } return mDmrRecordingViewerStage; } - public DMRRecordingViewer getDmrRecordingViewer() + public RecordingViewer getRecordingViewer() { - if(mDmrRecordingViewer == null) + if(mRecordingViewer == null) { - mDmrRecordingViewer = new DMRRecordingViewer(); + mRecordingViewer = new RecordingViewer(); } - return mDmrRecordingViewer; + return mRecordingViewer; } public Stage getIconManagerStage() @@ -453,12 +453,12 @@ public void process(final ViewChannelMapEditorRequest request) * Process a channel map editor request */ @Subscribe - public void process(final ViewDmrRecordingViewerRequest request) + public void process(final ViewRecordingViewerRequest request) { execute(() -> { - getDmrRecordingViewerStage().show(); - getDmrRecordingViewerStage().requestFocus(); - getDmrRecordingViewerStage().toFront(); + getRecordingViewerStage().show(); + getRecordingViewerStage().requestFocus(); + getRecordingViewerStage().toFront(); }); } diff --git a/src/main/java/io/github/dsheirer/gui/SDRTrunk.java b/src/main/java/io/github/dsheirer/gui/SDRTrunk.java index 0887b6453..fb18a0180 100644 --- a/src/main/java/io/github/dsheirer/gui/SDRTrunk.java +++ b/src/main/java/io/github/dsheirer/gui/SDRTrunk.java @@ -32,13 +32,13 @@ import io.github.dsheirer.controller.channel.ChannelException; import io.github.dsheirer.controller.channel.ChannelSelectionManager; import io.github.dsheirer.eventbus.MyEventBus; -import io.github.dsheirer.gui.dmr.ViewDmrRecordingViewerRequest; import io.github.dsheirer.gui.icon.ViewIconManagerRequest; import io.github.dsheirer.gui.playlist.ViewPlaylistRequest; import io.github.dsheirer.gui.preference.CalibrateRequest; import io.github.dsheirer.gui.preference.PreferenceEditorType; import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest; import io.github.dsheirer.gui.preference.calibration.CalibrationDialog; +import io.github.dsheirer.gui.viewer.ViewRecordingViewerRequest; import io.github.dsheirer.icon.IconModel; import io.github.dsheirer.log.ApplicationLog; import io.github.dsheirer.map.MapService; @@ -420,9 +420,9 @@ public void actionPerformed(ActionEvent event) viewMenu.add(new JSeparator()); - JMenuItem dmrMessageViewerMenu = new JMenuItem("DMR Recording Viewer"); - dmrMessageViewerMenu.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewDmrRecordingViewerRequest())); - viewMenu.add(dmrMessageViewerMenu); + JMenuItem recordingViewerMenu = new JMenuItem("Message Recording Viewer (.bits)"); + recordingViewerMenu.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewRecordingViewerRequest())); + viewMenu.add(recordingViewerMenu); JMenuItem settingsMenu = new JMenuItem("Icon Manager"); settingsMenu.addActionListener(arg0 -> MyEventBus.getGlobalEventBus().post(new ViewIconManagerRequest())); diff --git a/src/main/java/io/github/dsheirer/gui/dmr/DMRRecordingViewer.java b/src/main/java/io/github/dsheirer/gui/viewer/DmrViewer.java similarity index 85% rename from src/main/java/io/github/dsheirer/gui/dmr/DMRRecordingViewer.java rename to src/main/java/io/github/dsheirer/gui/viewer/DmrViewer.java index 0fd1adcbd..8c246ec69 100644 --- a/src/main/java/io/github/dsheirer/gui/dmr/DMRRecordingViewer.java +++ b/src/main/java/io/github/dsheirer/gui/viewer/DmrViewer.java @@ -17,20 +17,22 @@ * **************************************************************************** */ -package io.github.dsheirer.gui.dmr; +package io.github.dsheirer.gui.viewer; import io.github.dsheirer.message.IMessage; import io.github.dsheirer.module.decode.dmr.DMRMessageFramer; import io.github.dsheirer.module.decode.dmr.DMRMessageProcessor; import io.github.dsheirer.module.decode.dmr.DecodeConfigDMR; import io.github.dsheirer.record.binary.BinaryReader; +import io.github.dsheirer.util.ThreadPool; import java.io.File; import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; import java.util.Set; import java.util.TreeSet; import java.util.function.Predicate; import java.util.prefs.Preferences; -import javafx.application.Application; import javafx.application.Platform; import javafx.beans.property.SimpleStringProperty; import javafx.beans.value.ObservableValue; @@ -40,10 +42,10 @@ import javafx.collections.transformation.SortedList; import javafx.geometry.Insets; import javafx.geometry.Pos; -import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.CheckBox; import javafx.scene.control.Label; +import javafx.scene.control.ProgressIndicator; import javafx.scene.control.TableColumn; import javafx.scene.control.TablePosition; import javafx.scene.control.TableView; @@ -58,21 +60,19 @@ import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javafx.stage.FileChooser; -import javafx.stage.Stage; import javafx.util.Callback; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * Utility application to load and view a DMR .bits recording file with the messages fully parsed. + * DMR Viewer panel */ -public class DMRRecordingViewer extends VBox +public class DmrViewer extends VBox { - private static final Logger mLog = LoggerFactory.getLogger(DMRRecordingViewer.class); + private static final Logger mLog = LoggerFactory.getLogger(DmrViewer.class); private static final KeyCodeCombination KEY_CODE_COPY = new KeyCodeCombination(KeyCode.C, KeyCombination.CONTROL_ANY); - private static final String LAST_SELECTED_DIRECTORY = "last.selected.directory"; - private Preferences mPreferences = Preferences.userNodeForPackage(DMRRecordingViewer.class); - private Scene mScene; + private static final String LAST_SELECTED_DIRECTORY = "last.selected.directory.dmr"; + private Preferences mPreferences = Preferences.userNodeForPackage(DmrViewer.class); private Button mSelectFileButton; private Label mSelectedFileLabel; private TableView mMessageTableView; @@ -86,8 +86,9 @@ public class DMRRecordingViewer extends VBox private TextField mFindText; private Button mFindButton; private Button mFindNextButton; + private ProgressIndicator mLoadingIndicator; - public DMRRecordingViewer() + public DmrViewer() { setPadding(new Insets(5)); setSpacing(5); @@ -132,6 +133,21 @@ public DMRRecordingViewer() getChildren().addAll(fileBox, filterBox, getMessageTableView()); } + /** + * Spinny loading icon to show over the message table view + */ + private ProgressIndicator getLoadingIndicator() + { + if(mLoadingIndicator == null) + { + mLoadingIndicator = new ProgressIndicator(); + mLoadingIndicator.setProgress(-1); + mLoadingIndicator.setVisible(false); + } + + return mLoadingIndicator; + } + /** * Processes the recording file and loads the content into the viewer * @param file containing a .bits recording of decoded DMR data. @@ -140,33 +156,45 @@ private void load(File file) { if(file != null && file.exists()) { - getSelectedFileLabel().setText(file.getName()); mMessages.clear(); + getLoadingIndicator().setVisible(true); + getSelectedFileLabel().setText("Loading ..."); + final boolean useCompressed = getUseCompressedTalkgroups().isSelected(); - DMRMessageFramer messageFramer = new DMRMessageFramer(null); - DecodeConfigDMR config = new DecodeConfigDMR(); - if(getUseCompressedTalkgroups().isSelected()) - { - config.setUseCompressedTalkgroups(true); - } - DMRMessageProcessor messageProcessor = new DMRMessageProcessor(config); - messageFramer.setListener(messageProcessor); - messageProcessor.setMessageListener(message -> mMessages.add(message)); - - try(BinaryReader reader = new BinaryReader(file.toPath(), 200)) + ThreadPool.CACHED.submit(new Runnable() { - while(reader.hasNext()) + @Override + public void run() { - ByteBuffer buffer = reader.next(); - messageFramer.receive(buffer); - } - } - catch(Exception ioe) - { - ioe.printStackTrace(); - } + List messages = new ArrayList<>(); + DMRMessageFramer messageFramer = new DMRMessageFramer(null); + DecodeConfigDMR config = new DecodeConfigDMR(); + config.setUseCompressedTalkgroups(useCompressed); + DMRMessageProcessor messageProcessor = new DMRMessageProcessor(config); + messageFramer.setListener(messageProcessor); + messageProcessor.setMessageListener(message -> messages.add(message)); + + try(BinaryReader reader = new BinaryReader(file.toPath(), 200)) + { + while(reader.hasNext()) + { + ByteBuffer buffer = reader.next(); + messageFramer.receive(buffer); + } + } + catch(Exception ioe) + { + ioe.printStackTrace(); + } - getMessageTableView().scrollTo(0); + Platform.runLater(() -> { + getLoadingIndicator().setVisible(false); + getSelectedFileLabel().setText(file.getName()); + mMessages.addAll(messages); + getMessageTableView().scrollTo(0); + }); + } + }); } } @@ -176,9 +204,9 @@ private void load(File file) private void updateFilters() { Predicate timeslotPredicate = message -> - (getShowTS0().isSelected() && (message.getTimeslot() == 0)) || - (getShowTS1().isSelected() && (message.getTimeslot() == 1)) || - (getShowTS2().isSelected() && (message.getTimeslot() == 2)); + (getShowTS0().isSelected() && (message.getTimeslot() == 0)) || + (getShowTS1().isSelected() && (message.getTimeslot() == 1)) || + (getShowTS2().isSelected() && (message.getTimeslot() == 2)); String filterText = getSearchText().getText(); @@ -256,6 +284,7 @@ private TableView getMessageTableView() if(mMessageTableView == null) { mMessageTableView = new TableView<>(); + mMessageTableView.setPlaceholder(getLoadingIndicator()); SortedList sortedList = new SortedList<>(mFilteredMessages); sortedList.comparatorProperty().bind(mMessageTableView.comparatorProperty()); mMessageTableView.setItems(sortedList); @@ -509,37 +538,9 @@ private CheckBox getUseCompressedTalkgroups() { if(mUseCompressedTalkgroups == null) { - mUseCompressedTalkgroups = new CheckBox("Use Compressed Talkgroups"); + mUseCompressedTalkgroups = new CheckBox("Use Hytera Tier III Compressed Talkgroups"); } return mUseCompressedTalkgroups; } - - public static void main(String[] args) - { - Application viewer = new Application() - { - @Override - public void start(Stage primaryStage) throws Exception - { - Scene scene = new Scene(new DMRRecordingViewer(), 1100, 800); - primaryStage.setTitle("DMR Recording Viewer"); - primaryStage.setScene(scene); - primaryStage.show(); - } - }; - - Runnable r = () -> { - try - { - viewer.start(new Stage()); - } - catch(Exception e) - { - mLog.error("Error starting DMR recording viewer application", e); - } - }; - - Platform.startup(r); - } } diff --git a/src/main/java/io/github/dsheirer/gui/viewer/P25P1Viewer.java b/src/main/java/io/github/dsheirer/gui/viewer/P25P1Viewer.java new file mode 100644 index 000000000..997761e70 --- /dev/null +++ b/src/main/java/io/github/dsheirer/gui/viewer/P25P1Viewer.java @@ -0,0 +1,462 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 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.viewer; + +import io.github.dsheirer.message.IMessage; +import io.github.dsheirer.message.StuffBitsMessage; +import io.github.dsheirer.module.decode.p25.phase1.P25P1MessageFramer; +import io.github.dsheirer.module.decode.p25.phase1.P25P1MessageProcessor; +import io.github.dsheirer.record.binary.BinaryReader; +import io.github.dsheirer.util.ThreadPool; +import java.io.File; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.function.Predicate; +import java.util.prefs.Preferences; +import javafx.application.Platform; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.transformation.FilteredList; +import javafx.collections.transformation.SortedList; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ProgressIndicator; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TablePosition; +import javafx.scene.control.TableView; +import javafx.scene.control.TextField; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.scene.input.Clipboard; +import javafx.scene.input.ClipboardContent; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; +import javafx.scene.input.KeyCombination; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.stage.FileChooser; +import javafx.util.Callback; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * APCO25 Phase 1 viewer panel + */ +public class P25P1Viewer extends VBox +{ + private static final Logger mLog = LoggerFactory.getLogger(P25P1Viewer.class); + private static final KeyCodeCombination KEY_CODE_COPY = new KeyCodeCombination(KeyCode.C, KeyCombination.CONTROL_ANY); + private static final String LAST_SELECTED_DIRECTORY = "last.selected.directory.p25p1"; + private Preferences mPreferences = Preferences.userNodeForPackage(P25P1Viewer.class); + private Button mSelectFileButton; + private Label mSelectedFileLabel; + private TableView mMessageTableView; + private ObservableList mMessages = FXCollections.observableArrayList(); + private FilteredList mFilteredMessages = new FilteredList<>(mMessages); + private TextField mSearchText; + private TextField mFindText; + private Button mFindButton; + private Button mFindNextButton; + private ProgressIndicator mLoadingIndicator; + + public P25P1Viewer() + { + setPadding(new Insets(5)); + setSpacing(5); + + HBox fileBox = new HBox(); + fileBox.setMaxWidth(Double.MAX_VALUE); + fileBox.setAlignment(Pos.CENTER_LEFT); + fileBox.setSpacing(5); + HBox.setHgrow(getSelectFileButton(), Priority.NEVER); + HBox.setHgrow(getSelectedFileLabel(), Priority.ALWAYS); + getSelectedFileLabel().setAlignment(Pos.BASELINE_CENTER); + + fileBox.getChildren().addAll(getSelectFileButton(), getSelectedFileLabel()); + + HBox filterBox = new HBox(); + filterBox.setMaxWidth(Double.MAX_VALUE); + filterBox.setAlignment(Pos.BASELINE_CENTER); + filterBox.setSpacing(5); + + Label searchLabel = new Label("Message Filter:"); + HBox.setMargin(searchLabel, new Insets(0,0,0,15)); + Label findLabel = new Label("Find:"); + + HBox.setHgrow(getFindText(), Priority.ALWAYS); + HBox.setHgrow(getSearchText(), Priority.ALWAYS); + + filterBox.getChildren().addAll(findLabel, getFindText(), getFindButton(), getFindNextButton(), searchLabel, + getSearchText()); + + VBox.setVgrow(fileBox, Priority.NEVER); + VBox.setVgrow(filterBox, Priority.NEVER); + VBox.setVgrow(getMessageTableView(), Priority.ALWAYS); + + getChildren().addAll(fileBox, filterBox, getMessageTableView()); + } + + /** + * Processes the recording file and loads the content into the viewer + * + * Note: invoke this method off of the UI thread in a thread pool executor and the results will be loaded into the + * message table back on the JavaFX UI thread. + * + * @param file containing a .bits recording of decoded DMR data. + */ + private void load(File file) + { + if(file != null && file.exists()) + { + mMessages.clear(); + getLoadingIndicator().setVisible(true); + getSelectedFileLabel().setText("Loading ..."); + + ThreadPool.CACHED.submit(() -> { + List messages = new ArrayList<>(); + P25P1MessageFramer messageFramer = new P25P1MessageFramer(null, 9600); + P25P1MessageProcessor messageProcessor = new P25P1MessageProcessor(); + messageFramer.setListener(messageProcessor); + messageProcessor.setMessageListener(message -> { + if(!(message instanceof StuffBitsMessage)) + { + messages.add(message); + } + }); + + try(BinaryReader reader = new BinaryReader(file.toPath(), 200)) + { + while(reader.hasNext()) + { + ByteBuffer buffer = reader.next(); + messageFramer.receive(buffer); + } + } + catch(Exception ioe) + { + ioe.printStackTrace(); + } + + Platform.runLater(() -> { + getLoadingIndicator().setVisible(false); + getSelectedFileLabel().setText(file.getName()); + mMessages.addAll(messages); + getMessageTableView().scrollTo(0); + }); + }); + } + } + + /** + * Updates the filter(s) applies to the list of messages + */ + private void updateFilters() + { + String filterText = getSearchText().getText(); + + if(filterText != null && !filterText.isEmpty()) + { + Predicate textPredicate = message -> message.toString().toLowerCase().contains(filterText.toLowerCase()); + mFilteredMessages.setPredicate(textPredicate); + } + else + { + mFilteredMessages.setPredicate(null); + } + } + + /** + * Finds and selects the first row containing the text argument. + * @param text to search for. + */ + private void find(String text) + { + if(text != null && !text.isEmpty()) + { + for(IMessage message: mFilteredMessages) + { + if(message.toString().toLowerCase().contains(text.toLowerCase())) + { + getMessageTableView().getSelectionModel().select(message); + getMessageTableView().scrollTo(message); + return; + } + } + } + } + + /** + * Finds and selects the first row containing the text argument, after the currently selected row. + * @param text to search for. + */ + private void findNext(String text) + { + if(text != null && !text.isEmpty()) + { + IMessage selected = getMessageTableView().getSelectionModel().getSelectedItem(); + + if(selected == null) + { + find(text); + return; + } + + int row = mFilteredMessages.indexOf(selected); + + for(int x = row + 1; x < mFilteredMessages.size(); x++) + { + if(x < mFilteredMessages.size()) + { + IMessage message = mFilteredMessages.get(x); + + if(message.toString().toLowerCase().contains(text.toLowerCase())) + { + getMessageTableView().getSelectionModel().select(message); + getMessageTableView().scrollTo(message); + return; + } + } + } + } + } + + /** + * List view control with DMR messages + */ + private TableView getMessageTableView() + { + if(mMessageTableView == null) + { + mMessageTableView = new TableView<>(); + mMessageTableView.setPlaceholder(getLoadingIndicator()); + SortedList sortedList = new SortedList<>(mFilteredMessages); + sortedList.comparatorProperty().bind(mMessageTableView.comparatorProperty()); + mMessageTableView.setItems(sortedList); + + mMessageTableView.setOnKeyPressed(event -> + { + if(KEY_CODE_COPY.match(event)) + { + final Set rows = new TreeSet<>(); + for (final TablePosition tablePosition : mMessageTableView.getSelectionModel().getSelectedCells()) + { + rows.add(tablePosition.getRow()); + } + + final StringBuilder sb = new StringBuilder(); + boolean firstRow = true; + for (final Integer row : rows) + { + if(firstRow) + { + firstRow = false; + } + else + { + sb.append('\n'); + } + + boolean firstCol = true; + + for (final TableColumn column : mMessageTableView.getColumns()) + { + if(firstCol) + { + firstCol = false; + } + else + { + sb.append('\t'); + } + + final Object cellData = column.getCellData(row); + sb.append(cellData == null ? "" : cellData.toString()); + } + } + final ClipboardContent clipboardContent = new ClipboardContent(); + clipboardContent.putString(sb.toString()); + Clipboard.getSystemClipboard().setContent(clipboardContent); + } + }); + + TableColumn timestampColumn = new TableColumn(); + timestampColumn.setPrefWidth(110); + timestampColumn.setText("Time"); + timestampColumn.setCellValueFactory(new PropertyValueFactory<>("timestamp")); + + TableColumn validColumn = new TableColumn(); + validColumn.setPrefWidth(50); + validColumn.setText("Valid"); + validColumn.setCellValueFactory(new PropertyValueFactory<>("valid")); + + TableColumn messageColumn = new TableColumn(); + messageColumn.setPrefWidth(1000); + messageColumn.setText("Message"); + messageColumn.setCellValueFactory((Callback) param -> { + SimpleStringProperty property = new SimpleStringProperty(); + if(param.getValue() instanceof IMessage message) + { + property.set(message.toString()); + } + + return property; + }); + + mMessageTableView.getColumns().addAll(timestampColumn, validColumn, messageColumn); + } + + return mMessageTableView; + } + + /** + * File selection button + * @return button + */ + private Button getSelectFileButton() + { + if(mSelectFileButton == null) + { + mSelectFileButton = new Button("Select ..."); + mSelectFileButton.onActionProperty().set(event -> { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Select DMR .bits Recording"); + String lastDirectory = mPreferences.get(LAST_SELECTED_DIRECTORY, null); + if(lastDirectory != null) + { + File file = new File(lastDirectory); + if(file.exists() && file.isDirectory()) + { + fileChooser.setInitialDirectory(file); + } + } + fileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("sdrtrunk bits recording", "*.bits")); + final File selected = fileChooser.showOpenDialog(getScene().getWindow()); + + if(selected != null) + { + mPreferences.put(LAST_SELECTED_DIRECTORY, selected.getParent()); + load(selected); + } + }); + } + + return mSelectFileButton; + } + + /** + * Spinny loading icon to show over the message table view + */ + private ProgressIndicator getLoadingIndicator() + { + if(mLoadingIndicator == null) + { + mLoadingIndicator = new ProgressIndicator(); + mLoadingIndicator.setProgress(-1); + mLoadingIndicator.setVisible(false); + } + + return mLoadingIndicator; + } + + /** + * Selected file path label. + */ + private Label getSelectedFileLabel() + { + if(mSelectedFileLabel == null) + { + mSelectedFileLabel = new Label(" "); + } + + return mSelectedFileLabel; + } + + /** + * Search text filter box + * @return text control for entering search text + */ + private TextField getSearchText() + { + if(mSearchText == null) + { + mSearchText = new TextField(); + mSearchText.textProperty().addListener((observable, oldValue, newValue) -> updateFilters()); + } + + return mSearchText; + } + + /** + * Text box for find text + */ + private TextField getFindText() + { + if(mFindText == null) + { + mFindText = new TextField(); + mFindText.setOnKeyPressed(event -> { + if(event.getCode().equals(KeyCode.ENTER)) + { + getFindButton().fire(); + } + }); + mFindText.textProperty().addListener((observable, oldValue, newValue) -> updateFilters()); + } + + return mFindText; + } + + /** + * Find button to search for the text in the find text box. + * @return button + */ + private Button getFindButton() + { + if(mFindButton == null) + { + mFindButton = new Button("Find"); + mFindButton.setOnAction(event -> find(getFindText().getText())); + } + + return mFindButton; + } + + /** + * Find next button to search for the text in the find text box. + * @return button + */ + private Button getFindNextButton() + { + if(mFindNextButton == null) + { + mFindNextButton = new Button("Next"); + mFindNextButton.setOnAction(event -> findNext(getFindText().getText())); + } + + return mFindNextButton; + } +} diff --git a/src/main/java/io/github/dsheirer/gui/viewer/RecordingViewer.java b/src/main/java/io/github/dsheirer/gui/viewer/RecordingViewer.java new file mode 100644 index 000000000..16d797ba2 --- /dev/null +++ b/src/main/java/io/github/dsheirer/gui/viewer/RecordingViewer.java @@ -0,0 +1,181 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 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.viewer; + +import javafx.application.Application; +import javafx.application.Platform; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.control.Label; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuBar; +import javafx.scene.control.MenuItem; +import javafx.scene.control.SeparatorMenuItem; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; +import javafx.scene.control.TextField; +import javafx.scene.input.MouseButton; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.stage.Stage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Utility application to load and view .bits recording file with the messages fully parsed. + * + * Supported Protocols: DMR and APCO25 Phase 1. + */ +public class RecordingViewer extends VBox +{ + private static final Logger mLog = LoggerFactory.getLogger(RecordingViewer.class); + private MenuBar mMenuBar; + private TabPane mTabPane; + private int mTabCounterDmr = 1; + private int mTabCounterP25P1 = 1; + + /** + * Constructs an instance + */ + public RecordingViewer() + { + VBox.setVgrow(getTabPane(), Priority.ALWAYS); + getChildren().addAll(getMenuBar(), getTabPane()); + } + + public MenuBar getMenuBar() + { + if(mMenuBar == null) + { + mMenuBar = new MenuBar(); + Menu fileMenu = new Menu("File"); + + Menu createNewViewerMenu = new Menu("New Viewer ..."); + MenuItem dmrMenuItem = new MenuItem("DMR"); + dmrMenuItem.onActionProperty().set(event -> getTabPane().getTabs().add(new LabeledTab("DMR-" + mTabCounterDmr++, new DmrViewer()))); + MenuItem p25p1MenuItem = new MenuItem("P25P1"); + p25p1MenuItem.onActionProperty().set(event -> getTabPane().getTabs().add(new LabeledTab("P25P1-" + mTabCounterP25P1++, new P25P1Viewer()))); + createNewViewerMenu.getItems().addAll(dmrMenuItem, p25p1MenuItem); + + MenuItem exitMenu = new MenuItem("Exit"); + exitMenu.onActionProperty().set(event -> Platform.exit()); + fileMenu.getItems().addAll(createNewViewerMenu, new SeparatorMenuItem(), exitMenu); + mMenuBar.getMenus().add(fileMenu); + } + + return mMenuBar; + } + + /** + * Tab pane for each viewer instance + */ + public TabPane getTabPane() + { + if(mTabPane == null) + { + mTabPane = new TabPane(); + mTabPane.setMaxHeight(Double.MAX_VALUE); + mTabPane.getTabs().add(new LabeledTab("DMR-" + mTabCounterDmr++, new DmrViewer())); + mTabPane.getTabs().add(new LabeledTab("P25P1-" + mTabCounterP25P1++, new P25P1Viewer())); + } + + return mTabPane; + } + + /** + * Decorates the tab with label renaming feature using double-click or right-click to change text + */ + public class LabeledTab extends Tab + { + private Label mLabel = new Label(); + private TextField mTextField = new TextField(); + + /** + * Constructs an instance + * @param label to use initially + * @param node for content + */ + public LabeledTab(String label, Node node) + { + super(null, node); + + mLabel.setText(label); + setGraphic(mLabel); + + mLabel.setOnMouseClicked(event -> { + System.out.println("Clicked!"); + if((event.getButton().equals(MouseButton.PRIMARY) && event.getClickCount() > 1) || + event.getButton().equals(MouseButton.SECONDARY)) + { + setGraphic(mTextField); + mTextField.setText(mLabel.getText()); + mTextField.selectAll(); + mTextField.requestFocus(); + } + }); + + mTextField.setOnAction(event -> { + mLabel.setText(mTextField.getText()); + LabeledTab.this.setGraphic(mLabel); + }); + + mTextField.focusedProperty().addListener((observable, oldValue, newValue) -> { + if(!newValue) + { + mLabel.setText(mTextField.getText()); + LabeledTab.this.setGraphic(mLabel); + } + }); + } + + public LabeledTab(String label) + { + this(label, null); + } + } + + public static void main(String[] args) + { + Application viewer = new Application() + { + @Override + public void start(Stage primaryStage) throws Exception + { + Scene scene = new Scene(new RecordingViewer(), 1100, 800); + primaryStage.setTitle("BITS Recording Viewer"); + primaryStage.setScene(scene); + primaryStage.show(); + } + }; + + Runnable r = () -> { + try + { + viewer.start(new Stage()); + } + catch(Exception e) + { + mLog.error("Error starting recording viewer application", e); + } + }; + + Platform.startup(r); + } +} diff --git a/src/main/java/io/github/dsheirer/gui/dmr/ViewDmrRecordingViewerRequest.java b/src/main/java/io/github/dsheirer/gui/viewer/ViewRecordingViewerRequest.java similarity index 83% rename from src/main/java/io/github/dsheirer/gui/dmr/ViewDmrRecordingViewerRequest.java rename to src/main/java/io/github/dsheirer/gui/viewer/ViewRecordingViewerRequest.java index 1f3cba290..694598f4c 100644 --- a/src/main/java/io/github/dsheirer/gui/dmr/ViewDmrRecordingViewerRequest.java +++ b/src/main/java/io/github/dsheirer/gui/viewer/ViewRecordingViewerRequest.java @@ -17,17 +17,17 @@ * **************************************************************************** */ -package io.github.dsheirer.gui.dmr; +package io.github.dsheirer.gui.viewer; import io.github.dsheirer.gui.JavaFxWindowRequest; /** - * Request to show the DMR recording viewer + * Request to show the recording viewer */ -public class ViewDmrRecordingViewerRequest extends JavaFxWindowRequest +public class ViewRecordingViewerRequest extends JavaFxWindowRequest { /** * Constructs an instance */ - public ViewDmrRecordingViewerRequest() {} + public ViewRecordingViewerRequest() {} }