From 541ce53f170f1419659828cd12bfa09550611ac4 Mon Sep 17 00:00:00 2001 From: Joe Page Date: Mon, 10 Jun 2024 21:10:41 -0400 Subject: [PATCH 01/11] - move preferences into a single class --- .../devicemanager/MainApplication.java | 6 +- .../devicemanager/manager/DeviceManager.java | 11 +- .../devicemanager/ui/DeviceScreen.java | 62 +++---- .../devicemanager/ui/ExploreScreen.java | 167 ++++++------------ .../devicemanager/ui/InputScreen.java | 4 +- .../devicemanager/ui/LogsScreen.java | 44 ++--- .../ui/dialog/CommandDialog.java | 20 +-- .../ui/dialog/ConnectDialog.java | 30 ++-- .../ui/dialog/SettingsDialog.java | 32 ++-- .../devicemanager/utils/PreferenceUtils.java | 96 ++++++++++ .../devicemanager/utils/ResultWatcher.java | 9 + .../jpage4500/devicemanager/utils/Utils.java | 11 ++ 12 files changed, 241 insertions(+), 251 deletions(-) create mode 100644 src/main/java/com/jpage4500/devicemanager/utils/PreferenceUtils.java diff --git a/src/main/java/com/jpage4500/devicemanager/MainApplication.java b/src/main/java/com/jpage4500/devicemanager/MainApplication.java index 28a559d..e6d6876 100644 --- a/src/main/java/com/jpage4500/devicemanager/MainApplication.java +++ b/src/main/java/com/jpage4500/devicemanager/MainApplication.java @@ -4,7 +4,7 @@ import com.jpage4500.devicemanager.logging.AppLoggerFactory; import com.jpage4500.devicemanager.logging.Log; import com.jpage4500.devicemanager.ui.DeviceScreen; -import com.jpage4500.devicemanager.ui.dialog.SettingsDialog; +import com.jpage4500.devicemanager.utils.PreferenceUtils; import com.jpage4500.devicemanager.utils.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -14,7 +14,6 @@ import java.awt.*; import java.io.IOException; import java.util.Properties; -import java.util.prefs.Preferences; public class MainApplication { private static final Logger log = LoggerFactory.getLogger(MainApplication.class); @@ -58,8 +57,7 @@ private void setupLogging() { logger.setDebugLevel(Log.VERBOSE); logger.setLogToFile(true); - Preferences preferences = Preferences.userRoot(); - boolean isDebugMode = preferences.getBoolean(SettingsDialog.PREF_DEBUG_MODE, false); + boolean isDebugMode = PreferenceUtils.getPreference(PreferenceUtils.PrefBoolean.PREF_DEBUG_MODE, false); logger.setFileLogLevel(isDebugMode ? Log.DEBUG : Log.INFO); } diff --git a/src/main/java/com/jpage4500/devicemanager/manager/DeviceManager.java b/src/main/java/com/jpage4500/devicemanager/manager/DeviceManager.java index 39e3b1c..8991be0 100644 --- a/src/main/java/com/jpage4500/devicemanager/manager/DeviceManager.java +++ b/src/main/java/com/jpage4500/devicemanager/manager/DeviceManager.java @@ -6,10 +6,8 @@ import com.jpage4500.devicemanager.ui.ExploreScreen; import com.jpage4500.devicemanager.ui.dialog.ConnectDialog; import com.jpage4500.devicemanager.ui.dialog.SettingsDialog; -import com.jpage4500.devicemanager.utils.GsonHelper; -import com.jpage4500.devicemanager.utils.TextUtils; +import com.jpage4500.devicemanager.utils.*; import com.jpage4500.devicemanager.utils.Timer; -import com.jpage4500.devicemanager.utils.Utils; import se.vidstige.jadb.*; import se.vidstige.jadb.managers.PackageManager; import se.vidstige.jadb.managers.PropertyManager; @@ -485,8 +483,7 @@ private String checkFile(String path, String app) { public void captureScreenshot(Device device, TaskListener listener) { commandExecutorService.submit(() -> { - Preferences preferences = Preferences.userRoot(); - String downloadFolder = preferences.get(ExploreScreen.PREF_DOWNLOAD_FOLDER, System.getProperty("user.home")); + String downloadFolder = Utils.getDownloadFolder(); // 20211215-1441PM-1.png String name = new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date()) + ".png"; try { @@ -646,9 +643,7 @@ public interface TaskListener { public void downloadFile(Device device, String path, DeviceFile file, File saveFile, TaskListener listener) { log.debug("downloadFile: {}/{} -> {}", path, file.name, saveFile.getAbsolutePath()); - commandExecutorService.submit(() -> { - downloadFileInternal(device, path, file, saveFile); - }); + commandExecutorService.submit(() -> downloadFileInternal(device, path, file, saveFile)); } /** diff --git a/src/main/java/com/jpage4500/devicemanager/ui/DeviceScreen.java b/src/main/java/com/jpage4500/devicemanager/ui/DeviceScreen.java index a5cc05a..643c5ad 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/DeviceScreen.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/DeviceScreen.java @@ -35,7 +35,6 @@ import java.io.File; import java.util.List; import java.util.*; -import java.util.prefs.Preferences; /** * create and manage device view @@ -44,7 +43,6 @@ public class DeviceScreen extends BaseScreen implements DeviceManager.DeviceList private static final Logger log = LoggerFactory.getLogger(DeviceScreen.class); private static final String HINT_FILTER_DEVICES = "Filter devices..."; - public static final String PREF_ALWAYS_ON_TOP = "PREF_ALWAYS_ON_TOP"; public CustomTable table; public DeviceTableModel model; @@ -67,8 +65,9 @@ public DeviceScreen() { connectAdbServer(); - Preferences preferences = Preferences.userRoot(); - if (preferences.getBoolean(SettingsDialog.PREF_CHECK_UPDATES, true)) { + // check for updates (default: true) + boolean checkUpdates = PreferenceUtils.getPreference(PreferenceUtils.PrefBoolean.PREF_CHECK_UPDATES, true); + if (checkUpdates) { checkForUpdates(); } } @@ -167,24 +166,17 @@ private void setupMenuBar() { }); // [CMD + 2] = show explorer - createCmdAction(windowMenu, "Browse Files", KeyEvent.VK_2, e -> { - handleBrowseCommand(); - }); + createCmdAction(windowMenu, "Browse Files", KeyEvent.VK_2, e -> handleBrowseCommand()); // [CMD + 3] = show logs - createCmdAction(windowMenu, "View Logs", KeyEvent.VK_3, e -> { - handleLogsCommand(); - }); + createCmdAction(windowMenu, "View Logs", KeyEvent.VK_3, e -> handleLogsCommand()); // [CMD + T] = hide toolbar - createCmdAction(windowMenu, "Hide Toolbar", KeyEvent.VK_T, e -> { - hideToolbar(); - }); + createCmdAction(windowMenu, "Hide Toolbar", KeyEvent.VK_T, e -> hideToolbar()); // always on top JCheckBoxMenuItem onTopItem = new JCheckBoxMenuItem(); - Preferences preferences = Preferences.userRoot(); - boolean isAlwaysOnTop = preferences.getBoolean(PREF_ALWAYS_ON_TOP, false); + boolean isAlwaysOnTop = PreferenceUtils.getPreference(PreferenceUtils.PrefBoolean.PREF_ALWAYS_ON_TOP, false); setAlwaysOnTop(isAlwaysOnTop); onTopItem.setState(isAlwaysOnTop); onTopItem.setAction(new AbstractAction("Always on top") { @@ -192,7 +184,7 @@ private void setupMenuBar() { public void actionPerformed(ActionEvent actionEvent) { boolean alwaysOnTop = !isAlwaysOnTop(); setAlwaysOnTop(alwaysOnTop); - preferences.putBoolean(PREF_ALWAYS_ON_TOP, alwaysOnTop); + PreferenceUtils.setPreference(PreferenceUtils.PrefBoolean.PREF_ALWAYS_ON_TOP, alwaysOnTop); } }); windowMenu.add(onTopItem); @@ -200,14 +192,10 @@ public void actionPerformed(ActionEvent actionEvent) { JMenu deviceMenu = new JMenu("Devices"); // [CMD + F] = focus search box - createCmdAction(deviceMenu, "Filter", KeyEvent.VK_F, e -> { - filterTextField.requestFocus(); - }); + createCmdAction(deviceMenu, "Filter", KeyEvent.VK_F, e -> filterTextField.requestFocus()); // [CMD + N] = connect device - createCmdAction(deviceMenu, "Connect Device", KeyEvent.VK_N, e -> { - handleConnectDevice(); - }); + createCmdAction(deviceMenu, "Connect Device", KeyEvent.VK_N, e -> handleConnectDevice()); JMenuBar menubar = new JMenuBar(); menubar.add(windowMenu); @@ -367,13 +355,9 @@ private void setupSystemTray() { BufferedImage image = UiUtils.getImage("android.png", 40, 40); PopupMenu popup = new PopupMenu(); MenuItem openItem = new MenuItem("Open"); - openItem.addActionListener(e2 -> { - bringWindowToFront(); - }); + openItem.addActionListener(e2 -> bringWindowToFront()); MenuItem quitItem = new MenuItem("Quit"); - quitItem.addActionListener(e2 -> { - System.exit(0); - }); + quitItem.addActionListener(e2 -> System.exit(0)); popup.add(openItem); popup.add(quitItem); trayIcon = new TrayIcon(image, "Android Device Manager", popup); @@ -406,9 +390,7 @@ private void bringWindowToFront() { setState(JFrame.ICONIFIED); Utils.runDelayed(300, true, () -> { setState(JFrame.NORMAL); - Utils.runDelayed(300, true, () -> { - setState(JFrame.NORMAL); - }); + Utils.runDelayed(300, true, () -> setState(JFrame.NORMAL)); }); }); } @@ -462,8 +444,7 @@ private void handleHideColumn(int column) { if (columnType == null) return; List hiddenColList = SettingsDialog.getHiddenColumnList(); hiddenColList.add(columnType.name()); - Preferences preferences = Preferences.userRoot(); - preferences.put(SettingsDialog.PREF_HIDDEN_COLUMNS, GsonHelper.toJson(hiddenColList)); + PreferenceUtils.setPreference(PreferenceUtils.Pref.PREF_HIDDEN_COLUMNS, GsonHelper.toJson(hiddenColList)); model.setHiddenColumns(hiddenColList); } @@ -533,11 +514,14 @@ private void handleFilesDropped(List fileList) { showSelectDevicesDialog(); return; } + installOrCopyFiles(selectedDeviceList, fileList, null); + } + public void installOrCopyFiles(List selectedDeviceList, List fileList, DeviceManager.TaskListener listener) { boolean isApk = false; StringBuilder name = new StringBuilder(); for (File file : fileList) { - if (name.length() > 0) name.append(", "); + if (!name.isEmpty()) name.append(", "); String filename = file.getName(); name.append(filename); if (filename.endsWith(".apk")) { @@ -561,6 +545,7 @@ private void handleFilesDropped(List fileList) { log.debug("handleFilesDropped: installing: {}", name); ResultWatcher resultWatcher = new ResultWatcher(selectedDeviceList.size() * fileList.size()); + resultWatcher.setListener(listener); for (Device device : selectedDeviceList) { for (File file : fileList) { String filename = file.getName(); @@ -574,7 +559,7 @@ private void handleFilesDropped(List fileList) { resultWatcher.handleResult(getRootPane(), result); }); } else { - // TODO: where to put files? + // TODO: where to put files on device? DeviceManager.getInstance().copyFile(device, file, "/sdcard/Download/", (isSuccess, error) -> { setDeviceBusy(device, false); String result = "COPY: " + filename + " -> " + @@ -849,9 +834,7 @@ private void handleRestartCommand() { if (rc != JOptionPane.YES_OPTION) return; for (Device device : selectedDeviceList) { - DeviceManager.getInstance().restartDevice(device, (isSuccess, error) -> { - refreshDevices(); - }); + DeviceManager.getInstance().restartDevice(device, (isSuccess, error) -> refreshDevices()); } } @@ -873,8 +856,7 @@ private void handleInstallCommand() { return; } - Preferences preferences = Preferences.userRoot(); - String downloadFolder = preferences.get(ExploreScreen.PREF_DOWNLOAD_FOLDER, System.getProperty("user.home")); + String downloadFolder = Utils.getDownloadFolder(); JFileChooser chooser = new JFileChooser(); chooser.setCurrentDirectory(new File(downloadFolder)); diff --git a/src/main/java/com/jpage4500/devicemanager/ui/ExploreScreen.java b/src/main/java/com/jpage4500/devicemanager/ui/ExploreScreen.java index 7a12053..a37f5c1 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/ExploreScreen.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/ExploreScreen.java @@ -28,7 +28,6 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; -import java.util.prefs.Preferences; /** * create and manage device view @@ -36,11 +35,8 @@ public class ExploreScreen extends BaseScreen { private static final Logger log = LoggerFactory.getLogger(ExploreScreen.class); - public static final String PREF_DOWNLOAD_FOLDER = "PREF_DOWNLOAD_FOLDER"; - public static final String PREF_GO_TO_FOLDER_LIST = "PREF_GO_TO_FOLDER_LIST"; private static final String HINT_FILTER_DEVICES = "Filter files..."; public static final int MAX_PATH_SAVE = 10; - public static final String PREF_USE_ROOT = "PREF_USE_ROOT"; private final DeviceScreen deviceScreen; @@ -134,36 +130,24 @@ private void setupMenuBar() { JMenu windowMenu = new JMenu("Window"); // [CMD + W] = close window - createCmdAction(windowMenu, "Close Window", KeyEvent.VK_W, e -> { - closeWindow(); - }); + createCmdAction(windowMenu, "Close Window", KeyEvent.VK_W, e -> closeWindow()); // [CMD + 1] = show devices - createCmdAction(windowMenu, "Show Devices", KeyEvent.VK_1, e -> { - deviceScreen.toFront(); - }); + createCmdAction(windowMenu, "Show Devices", KeyEvent.VK_1, e -> deviceScreen.toFront()); // [CMD + 3] = show logs - createCmdAction(windowMenu, "View Logs", KeyEvent.VK_3, e -> { - deviceScreen.handleLogsCommand(); - }); + createCmdAction(windowMenu, "View Logs", KeyEvent.VK_3, e -> deviceScreen.handleLogsCommand()); // [CMD + T] = hide toolbar - createCmdAction(windowMenu, "Hide Toolbar", KeyEvent.VK_T, e -> { - hideToolbar(); - }); + createCmdAction(windowMenu, "Hide Toolbar", KeyEvent.VK_T, e -> hideToolbar()); JMenu fileMenu = new JMenu("Files"); // [CMD + BACKSPACE] = delete files - createCmdAction(fileMenu, "Delete", KeyEvent.VK_BACK_SPACE, e -> { - handleDelete(); - }); + createCmdAction(fileMenu, "Delete", KeyEvent.VK_BACK_SPACE, e -> handleDelete()); // [CMD + G] = go to folder - createCmdAction(fileMenu, "Go to folder..", KeyEvent.VK_G, e -> { - handleGoToFolder(); - }); + createCmdAction(fileMenu, "Go to folder..", KeyEvent.VK_G, e -> handleGoToFolder()); JMenuBar menubar = new JMenuBar(); menubar.add(windowMenu); @@ -308,45 +292,43 @@ private void setPath(String path) { private void refreshFiles() { if (!device.isOnline) return; - DeviceManager.getInstance().listFiles(device, selectedPath, useRoot, (fileList, error) -> { - SwingUtilities.invokeLater(() -> { - if (error != null) { - errorMessage = error; - if (useRoot && TextUtils.equals(error, DeviceManager.ERR_ROOT_NOT_AVAILABLE)) { - JOptionPane.showMessageDialog(this, "ROOT not available!"); - toggleRoot(); - } else if (TextUtils.equals(error, DeviceManager.ERR_PERMISSION_DENIED)) { - errorMessage = "permission denied"; - // revert to previous directory - setPath(null); - } - refreshUi(); - return; - } - if (fileList == null) { - log.debug("refreshFiles: NO FILES"); - errorMessage = "permission denied - " + selectedPath; + DeviceManager.getInstance().listFiles(device, selectedPath, useRoot, (fileList, error) -> SwingUtilities.invokeLater(() -> { + if (error != null) { + errorMessage = error; + if (useRoot && TextUtils.equals(error, DeviceManager.ERR_ROOT_NOT_AVAILABLE)) { + JOptionPane.showMessageDialog(this, "ROOT not available!"); + toggleRoot(); + } else if (TextUtils.equals(error, DeviceManager.ERR_PERMISSION_DENIED)) { + errorMessage = "permission denied"; + // revert to previous directory setPath(null); - log.trace("refreshFiles: selectedPath={}", selectedPath); - } else { - // clear out any previous set filter and error - filterTextField.reset(); - errorMessage = null; - // add ".." to top of list - if (!TextUtils.isEmpty(selectedPath) && !selectedPath.equals("/")) { - DeviceFile upFile = new DeviceFile(); - upFile.name = ".."; - upFile.isDirectory = true; - fileList.add(0, upFile); - } - // TODO: backup selected file(s) - model.setFileList(fileList); - // TODO: re-select previously selected file(s) - table.changeSelection(0, 0, true, false); } refreshUi(); - }); - }); + return; + } + if (fileList == null) { + log.debug("refreshFiles: NO FILES"); + errorMessage = "permission denied - " + selectedPath; + setPath(null); + log.trace("refreshFiles: selectedPath={}", selectedPath); + } else { + // clear out any previous set filter and error + filterTextField.reset(); + errorMessage = null; + // add ".." to top of list + if (!TextUtils.isEmpty(selectedPath) && !selectedPath.equals("/")) { + DeviceFile upFile = new DeviceFile(); + upFile.name = ".."; + upFile.isDirectory = true; + fileList.add(0, upFile); + } + // TODO: backup selected file(s) + model.setFileList(fileList); + // TODO: re-select previously selected file(s) + table.changeSelection(0, 0, true, false); + } + refreshUi(); + })); } private void refreshUi() { @@ -389,41 +371,11 @@ private void setupPopupMenu() { private void handleFilesDropped(List fileList) { if (!device.isOnline) return; - boolean isApk = false; - StringBuilder name = new StringBuilder(); - for (File file : fileList) { - if (name.length() > 0) name.append(", "); - String filename = file.getName(); - name.append(filename); - if (filename.endsWith(".apk")) { - isApk = true; - break; - } - } - - String title = isApk ? "Install App" : "Copy File"; - String msg = isApk ? "Install " : "Copy "; - msg += name.toString(); - msg += " to " + selectedPath; - msg += "?"; - - // prompt to install/copy - // NOTE: using JDialog.setAlwaysOnTap to bring app to foreground on drag and drop operations - final JDialog dialog = new JDialog(); - dialog.setAlwaysOnTop(true); - int rc = JOptionPane.showConfirmDialog(dialog, msg, title, JOptionPane.YES_NO_OPTION); - if (rc != JOptionPane.YES_OPTION) return; - - for (File file : fileList) { - String filename = file.getName(); - if (filename.endsWith(".apk")) { - DeviceManager.getInstance().installApp(device, file, null); - } else { - DeviceManager.getInstance().copyFile(device, file, selectedPath + "/", (isSuccess, error) -> { - if (isSuccess) refreshFiles(); - }); - } - } + List deviceList = new ArrayList<>(); + deviceList.add(device); + deviceScreen.installOrCopyFiles(deviceList, fileList, (isSuccess, error) -> { + if (isSuccess) refreshFiles(); + }); } private void setupToolbar() { @@ -446,18 +398,14 @@ private void setupToolbar() { createToolbarButton(toolbar, "icon_refresh.png", "Refresh", "Refresh Files", actionEvent -> refreshFiles()); // root toolbar button - Preferences preferences = Preferences.userRoot(); - useRoot = preferences.getBoolean(PREF_USE_ROOT, false); - rootButton = createToolbarButton(toolbar, "root.png", "Root", "Root Mode", actionEvent -> { - toggleRoot(); - }); + useRoot = PreferenceUtils.getPreference(PreferenceUtils.PrefBoolean.PREF_USE_ROOT, false); + rootButton = createToolbarButton(toolbar, "root.png", "Root", "Root Mode", actionEvent -> toggleRoot()); refreshRootButton(); } private void toggleRoot() { - Preferences preferences = Preferences.userRoot(); useRoot = !useRoot; - preferences.putBoolean(PREF_USE_ROOT, useRoot); + PreferenceUtils.setPreference(PreferenceUtils.PrefBoolean.PREF_USE_ROOT, useRoot); refreshRootButton(); refreshFiles(); } @@ -479,9 +427,7 @@ private void handleNewFolder() { null); if (TextUtils.isEmpty(result)) return; - DeviceManager.getInstance().createFolder(device, selectedPath + "/" + result, (isSuccess, error) -> { - refreshFiles(); - }); + DeviceManager.getInstance().createFolder(device, selectedPath + "/" + result, (isSuccess, error) -> refreshFiles()); } private void handleDownload() { @@ -503,11 +449,7 @@ private void handleDownload() { "Download?", JOptionPane.YES_NO_OPTION); if (rc != JOptionPane.YES_OPTION) return; - Preferences preferences = Preferences.userRoot(); - String downloadFolder = preferences.get(ExploreScreen.PREF_DOWNLOAD_FOLDER, null); - if (downloadFolder == null) { - downloadFolder = System.getProperty("user.home") + "/Downloads"; - } + String downloadFolder = Utils.getDownloadFolder(); for (DeviceFile file : selectedFileList) { if (file.isReadOnly) { JOptionPane.showMessageDialog(this, "File is read-only!", "Read-only", JOptionPane.ERROR_MESSAGE); @@ -549,15 +491,12 @@ private void handleDelete() { if (rc != JOptionPane.YES_OPTION) return; for (DeviceFile file : selectedFileList) { - DeviceManager.getInstance().deleteFile(device, selectedPath, file, (isSuccess, error) -> { - refreshFiles(); - }); + DeviceManager.getInstance().deleteFile(device, selectedPath, file, (isSuccess, error) -> refreshFiles()); } } private void handleGoToFolder() { - Preferences preferences = Preferences.userRoot(); - String folders = preferences.get(PREF_GO_TO_FOLDER_LIST, null); + String folders = PreferenceUtils.getPreference(PreferenceUtils.Pref.PREF_GO_TO_FOLDER_LIST); List customList = GsonHelper.stringToList(folders, String.class); // List selectedList = new ArrayList<>(); @@ -584,7 +523,7 @@ private void handleGoToFolder() { if (customList.size() > 10) { customList = customList.subList(0, 10); } - preferences.put(PREF_GO_TO_FOLDER_LIST, GsonHelper.toJson(customList)); + PreferenceUtils.setPreference(PreferenceUtils.Pref.PREF_GO_TO_FOLDER_LIST, GsonHelper.toJson(customList)); log.debug("handleGoToFolder: {}", selectedItem); setPath(selectedItem); refreshFiles(); diff --git a/src/main/java/com/jpage4500/devicemanager/ui/InputScreen.java b/src/main/java/com/jpage4500/devicemanager/ui/InputScreen.java index 68fe4c6..cfef3d9 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/InputScreen.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/InputScreen.java @@ -152,9 +152,7 @@ public void keyPressed(KeyEvent e) { panel.add(textField, "growx, span 2, wrap"); JButton sendButton = new JButton("Send"); - sendButton.addActionListener(e -> { - handleEnterPressed(); - }); + sendButton.addActionListener(e -> handleEnterPressed()); panel.add(sendButton, "al right, span 2, wrap"); frame.setContentPane(panel); diff --git a/src/main/java/com/jpage4500/devicemanager/ui/LogsScreen.java b/src/main/java/com/jpage4500/devicemanager/ui/LogsScreen.java index 8e3fffa..bc51d81 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/LogsScreen.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/LogsScreen.java @@ -164,19 +164,13 @@ private void setupMenuBar() { }); // [CMD + 1] = show devices - createCmdAction(windowMenu, "Show Devices", KeyEvent.VK_1, e -> { - deviceScreen.toFront(); - }); + createCmdAction(windowMenu, "Show Devices", KeyEvent.VK_1, e -> deviceScreen.toFront()); // [CMD + 2] = show explorer - createCmdAction(windowMenu, "Browse Files", KeyEvent.VK_2, e -> { - deviceScreen.handleBrowseCommand(); - }); + createCmdAction(windowMenu, "Browse Files", KeyEvent.VK_2, e -> deviceScreen.handleBrowseCommand()); // [CMD + T] = hide toolbar - createCmdAction(windowMenu, "Hide Toolbar", KeyEvent.VK_T, e -> { - hideToolbar(); - }); + createCmdAction(windowMenu, "Hide Toolbar", KeyEvent.VK_T, e -> hideToolbar()); JMenu logsMenu = new JMenu("Logs"); @@ -193,29 +187,19 @@ private void setupMenuBar() { }); // [CMD + KEY_DOWN] = scroll to bottom - createCmdAction(logsMenu, "Scoll to bottom", KeyEvent.VK_DOWN, e -> { - table.scrollToBottom(); - }); + createCmdAction(logsMenu, "Scoll to bottom", KeyEvent.VK_DOWN, e -> table.scrollToBottom()); // [CMD + KEY_UP] = page up - createOptionAction(logsMenu, "Page Up", KeyEvent.VK_UP, e -> { - table.pageUp(); - }); + createOptionAction(logsMenu, "Page Up", KeyEvent.VK_UP, e -> table.pageUp()); // [CMD + KE_DOWN] = page down - createOptionAction(logsMenu, "Page Down", KeyEvent.VK_DOWN, e -> { - table.pageDown(); - }); + createOptionAction(logsMenu, "Page Down", KeyEvent.VK_DOWN, e -> table.pageDown()); // [CMD + K] = clear logs - createCmdAction(logsMenu, "Clear logs", KeyEvent.VK_K, e -> { - model.clearLogs(); - }); + createCmdAction(logsMenu, "Clear logs", KeyEvent.VK_K, e -> model.clearLogs()); // [CMD + F] = focus search field - createCmdAction(windowMenu, "Search for...", KeyEvent.VK_1, e -> { - searchField.requestFocus(); - }); + createCmdAction(windowMenu, "Search for...", KeyEvent.VK_1, e -> searchField.requestFocus()); JMenuBar menubar = new JMenuBar(); menubar.add(windowMenu); @@ -361,9 +345,7 @@ private void refreshUi() { private void setupToolbar() { toolbar.setRollover(true); - logButton = createSmallToolbarButton(toolbar, null, null, "Start Logging", actionEvent -> { - toggleLoggingButton(); - }); + logButton = createSmallToolbarButton(toolbar, null, null, "Start Logging", actionEvent -> toggleLoggingButton()); updateLoggingButton(); toolbar.add(Box.createHorizontalGlue()); @@ -427,9 +409,7 @@ private void setupFilterList() { filterList.setListData(filterItemList.toArray(new FilterItem[0])); filterList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - filterList.addListSelectionListener(e -> { - filterDevices(filterField.getCleanText()); - }); + filterList.addListSelectionListener(e -> filterDevices(filterField.getCleanText())); } private void addLogLevel(List filterItemList, String label, String filter) { @@ -480,9 +460,7 @@ public void handleLogEntries(List logEntryList) { @Override public void handleProcessMap(Map processMap) { - SwingUtilities.invokeLater(() -> { - model.setProcessMap(processMap); - }); + SwingUtilities.invokeLater(() -> model.setProcessMap(processMap)); } } diff --git a/src/main/java/com/jpage4500/devicemanager/ui/dialog/CommandDialog.java b/src/main/java/com/jpage4500/devicemanager/ui/dialog/CommandDialog.java index e7a43f4..90372a9 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/dialog/CommandDialog.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/dialog/CommandDialog.java @@ -4,6 +4,7 @@ import com.jpage4500.devicemanager.manager.DeviceManager; import com.jpage4500.devicemanager.table.utils.AlternatingBackgroundColorRenderer; import com.jpage4500.devicemanager.utils.GsonHelper; +import com.jpage4500.devicemanager.utils.PreferenceUtils; import com.jpage4500.devicemanager.utils.ResultWatcher; import com.jpage4500.devicemanager.utils.TextUtils; import net.miginfocom.swing.MigLayout; @@ -17,12 +18,12 @@ import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.List; -import java.util.prefs.Preferences; + +import static com.jpage4500.devicemanager.utils.PreferenceUtils.Pref; public class CommandDialog extends JPanel { private static final Logger log = LoggerFactory.getLogger(CommandDialog.class); - public static final String PREF_CUSTOM_COMMAND_LIST = "PREF_CUSTOM_COMMAND_LIST"; public static final int MAX_RECENT_COMMANDS = 10; private Component frame; @@ -106,18 +107,15 @@ public void keyPressed(KeyEvent e) { add(textField, "growx, span 2, wrap"); JButton sendButton = new JButton("Send Command"); - sendButton.addActionListener(e -> { - handleEnterPressed(); - }); + sendButton.addActionListener(e -> handleEnterPressed()); add(sendButton, "al right, span 2, wrap"); } private void deleteItem(String command) { - log.debug("deleteItem: {}", command); + log.trace("deleteItem: {}", command); List customCommands = getCustomCommands(); customCommands.remove(command); - Preferences preferences = Preferences.userRoot(); - preferences.put(PREF_CUSTOM_COMMAND_LIST, GsonHelper.toJson(customCommands)); + PreferenceUtils.setPreference(Pref.PREF_CUSTOM_COMMAND_LIST, GsonHelper.toJson(customCommands)); populateRecent(); } @@ -144,8 +142,7 @@ private void handleEnterPressed() { customCommands = customCommands.subList(0, MAX_RECENT_COMMANDS); } - Preferences preferences = Preferences.userRoot(); - preferences.put(PREF_CUSTOM_COMMAND_LIST, GsonHelper.toJson(customCommands)); + PreferenceUtils.setPreference(Pref.PREF_CUSTOM_COMMAND_LIST, GsonHelper.toJson(customCommands)); // update displayed list populateRecent(); @@ -167,8 +164,7 @@ private void populateRecent() { } private List getCustomCommands() { - Preferences preferences = Preferences.userRoot(); - String customCommands = preferences.get(PREF_CUSTOM_COMMAND_LIST, null); + String customCommands = PreferenceUtils.getPreference(Pref.PREF_CUSTOM_COMMAND_LIST); List commandList = GsonHelper.stringToList(customCommands, String.class); if (commandList.isEmpty()) { // add some common commands diff --git a/src/main/java/com/jpage4500/devicemanager/ui/dialog/ConnectDialog.java b/src/main/java/com/jpage4500/devicemanager/ui/dialog/ConnectDialog.java index 071474e..0e51971 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/dialog/ConnectDialog.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/dialog/ConnectDialog.java @@ -4,6 +4,7 @@ import com.jpage4500.devicemanager.manager.DeviceManager; import com.jpage4500.devicemanager.table.utils.AlternatingBackgroundColorRenderer; import com.jpage4500.devicemanager.utils.GsonHelper; +import com.jpage4500.devicemanager.utils.PreferenceUtils; import com.jpage4500.devicemanager.utils.TextUtils; import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; @@ -18,11 +19,10 @@ import java.util.List; import java.util.prefs.Preferences; +import static com.jpage4500.devicemanager.utils.PreferenceUtils.Pref; + public class ConnectDialog extends JPanel { private static final Logger log = LoggerFactory.getLogger(ConnectDialog.class); - public static final String PREF_RECENT_WIRELESS_DEVICES = "PREF_RECENT_WIRELESS_DEVICES"; - public static final String PREF_LAST_DEVICE_IP = "PREF_LAST_DEVICE_IP"; - public static final String PREF_LAST_DEVICE_PORT = "PREF_LAST_DEVICE_PORT"; private JTextField serverField; private JTextField portField; @@ -47,9 +47,9 @@ public static void showConnectDialog(Component frame, DeviceManager.TaskListener log.error("Invalid port: " + screen.portField.getText()); return; } - Preferences preferences = Preferences.userRoot(); - preferences.put(PREF_LAST_DEVICE_IP, ip); - preferences.put(PREF_LAST_DEVICE_PORT, String.valueOf(port)); + + PreferenceUtils.setPreference(PreferenceUtils.Pref.PREF_LAST_DEVICE_IP, ip); + PreferenceUtils.setPreference(PreferenceUtils.PrefInt.PREF_LAST_DEVICE_PORT, port); DeviceManager deviceManager = DeviceManager.getInstance(); deviceManager.connectDevice(ip, port, listener); @@ -60,9 +60,10 @@ private ConnectDialog() { List deviceList = getRecentWirelessDevices(); - Preferences preferences = Preferences.userRoot(); - String lastIp = preferences.get(PREF_LAST_DEVICE_IP, "192.168.0.1"); - String lastPort = preferences.get(PREF_LAST_DEVICE_PORT, "5555"); + String lastIp = PreferenceUtils.getPreference(PreferenceUtils.Pref.PREF_LAST_DEVICE_IP); + int lastPort = PreferenceUtils.getPreference(PreferenceUtils.PrefInt.PREF_LAST_DEVICE_PORT, 5555); + + if (TextUtils.isEmpty(lastIp)) lastIp = "192.168.0.1"; add(new JLabel("Recent Devices"), "growx, span 2, wrap"); @@ -87,7 +88,7 @@ public void focusLost(FocusEvent e) { add(new JSeparator(), "growx, spanx, wrap"); serverField = new JTextField(lastIp); - portField = new JTextField(lastPort); + portField = new JTextField(String.valueOf(lastPort)); serverField.setHorizontalAlignment(SwingConstants.RIGHT); portField.setHorizontalAlignment(SwingConstants.RIGHT); @@ -141,8 +142,7 @@ public void keyTyped(KeyEvent e) { } public static List getRecentWirelessDevices() { - Preferences preferences = Preferences.userRoot(); - String recentDeviceStr = preferences.get(PREF_RECENT_WIRELESS_DEVICES, null); + String recentDeviceStr = PreferenceUtils.getPreference(Pref.PREF_RECENT_WIRELESS_DEVICES); return GsonHelper.stringToList(recentDeviceStr, WirelessDevice.class); } @@ -162,15 +162,13 @@ public static void addWirelessDevice(Device device) { if (deviceList.size() > 10) { deviceList.remove(deviceList.size() - 1); } - Preferences preferences = Preferences.userRoot(); - preferences.put(PREF_RECENT_WIRELESS_DEVICES, GsonHelper.toJson(deviceList)); + PreferenceUtils.setPreference(Pref.PREF_RECENT_WIRELESS_DEVICES, GsonHelper.toJson(deviceList)); } public static void removeWirelessDevice(WirelessDevice wirelessDevice) { List deviceList = getRecentWirelessDevices(); deviceList.removeIf(device -> TextUtils.equals(device.serial, wirelessDevice.serial)); - Preferences preferences = Preferences.userRoot(); - preferences.put(PREF_RECENT_WIRELESS_DEVICES, GsonHelper.toJson(deviceList)); + PreferenceUtils.setPreference(Pref.PREF_RECENT_WIRELESS_DEVICES, GsonHelper.toJson(deviceList)); } } diff --git a/src/main/java/com/jpage4500/devicemanager/ui/dialog/SettingsDialog.java b/src/main/java/com/jpage4500/devicemanager/ui/dialog/SettingsDialog.java index 5d5a2b5..9a31683 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/dialog/SettingsDialog.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/dialog/SettingsDialog.java @@ -3,9 +3,9 @@ import com.jpage4500.devicemanager.logging.AppLoggerFactory; import com.jpage4500.devicemanager.logging.Log; import com.jpage4500.devicemanager.table.DeviceTableModel; -import com.jpage4500.devicemanager.ui.ExploreScreen; import com.jpage4500.devicemanager.ui.views.CheckBoxList; import com.jpage4500.devicemanager.utils.GsonHelper; +import com.jpage4500.devicemanager.utils.PreferenceUtils; import com.jpage4500.devicemanager.utils.Utils; import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; @@ -25,10 +25,6 @@ public class SettingsDialog extends JPanel { private static final Logger log = LoggerFactory.getLogger(SettingsDialog.class); - public static final String PREF_CUSTOM_APPS = "PREF_CUSTOM_APPS"; - public static final String PREF_DEBUG_MODE = "PREF_DEBUG_MODE"; - public static final String PREF_HIDDEN_COLUMNS = "PREF_HIDDEN_COLUMNS"; - public static final String PREF_CHECK_UPDATES = "PREF_CHECK_UPDATES"; private Component frame; private DeviceTableModel tableModel; @@ -46,7 +42,6 @@ private SettingsDialog(Component frame, DeviceTableModel tableModel) { this.frame = frame; this.tableModel = tableModel; - Preferences preferences = Preferences.userRoot(); setLayout(new MigLayout("", "[][]")); // columns @@ -83,26 +78,26 @@ public void mouseClicked(MouseEvent e) { add(appButton, "wrap"); JCheckBox updateCheckbox = new JCheckBox("Check for updates"); - boolean checkUpdates = preferences.getBoolean(SettingsDialog.PREF_CHECK_UPDATES, true); + boolean checkUpdates = PreferenceUtils.getPreference(PreferenceUtils.PrefBoolean.PREF_CHECK_UPDATES, true); updateCheckbox.setSelected(checkUpdates); updateCheckbox.setHorizontalTextPosition(SwingConstants.LEFT); updateCheckbox.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { - preferences.putBoolean(PREF_CHECK_UPDATES, updateCheckbox.isSelected()); + PreferenceUtils.setPreference(PreferenceUtils.PrefBoolean.PREF_CHECK_UPDATES, updateCheckbox.isSelected()); } }); add(updateCheckbox, "span 2, al right, wrap"); add(new JSeparator(), "growx, spanx, wrap"); - boolean isDebugMode = preferences.getBoolean(PREF_DEBUG_MODE, false); + boolean isDebugMode = PreferenceUtils.getPreference(PreferenceUtils.PrefBoolean.PREF_DEBUG_MODE, false); debugCheckbox = new JCheckBox("Debug Mode"); debugCheckbox.setHorizontalTextPosition(SwingConstants.LEFT); debugCheckbox.setSelected(isDebugMode); debugCheckbox.addChangeListener(e -> { boolean updatedValue = debugCheckbox.isSelected(); - preferences.putBoolean(PREF_DEBUG_MODE, updatedValue); + PreferenceUtils.setPreference(PreferenceUtils.PrefBoolean.PREF_DEBUG_MODE, updatedValue); AppLoggerFactory logger = (AppLoggerFactory) LoggerFactory.getILoggerFactory(); logger.setFileLogLevel(updatedValue ? Log.DEBUG : Log.INFO); refreshUi(); @@ -169,8 +164,7 @@ private void viewLogs() { } public static List getHiddenColumnList() { - Preferences preferences = Preferences.userRoot(); - String hiddenColsStr = preferences.get(PREF_HIDDEN_COLUMNS, null); + String hiddenColsStr = PreferenceUtils.getPreference(PreferenceUtils.Pref.PREF_HIDDEN_COLUMNS); return GsonHelper.stringToList(hiddenColsStr, String.class); } @@ -196,8 +190,7 @@ private void showColumns() { // save columns that are NOT selected List selectedItems = checkBoxList.getUnSelectedItems(); log.debug("HIDDEN: {}", GsonHelper.toJson(selectedItems)); - Preferences preferences = Preferences.userRoot(); - preferences.put(PREF_HIDDEN_COLUMNS, GsonHelper.toJson(selectedItems)); + PreferenceUtils.setPreference(PreferenceUtils.Pref.PREF_HIDDEN_COLUMNS, GsonHelper.toJson(selectedItems)); tableModel.setHiddenColumns(selectedItems); } @@ -206,8 +199,7 @@ private void showAppsSettings() { List resultList = showMultilineEditDialog("Custom Apps", "Enter package name(s) to track - 1 per line", appList); if (resultList == null) return; - Preferences preferences = Preferences.userRoot(); - preferences.put(PREF_CUSTOM_APPS, GsonHelper.toJson(resultList)); + PreferenceUtils.setPreference(PreferenceUtils.Pref.PREF_CUSTOM_APPS, GsonHelper.toJson(resultList)); tableModel.setAppList(resultList); } @@ -215,8 +207,7 @@ private void showAppsSettings() { * get list of custom monitored apps */ public static List getCustomApps() { - Preferences preferences = Preferences.userRoot(); - String appPrefs = preferences.get(PREF_CUSTOM_APPS, null); + String appPrefs = PreferenceUtils.getPreference(PreferenceUtils.Pref.PREF_CUSTOM_APPS); return GsonHelper.stringToList(appPrefs, String.class); } @@ -267,8 +258,7 @@ private String showSingleLineEditDialog(String title, String message, String val } private void showDownloadLocation() { - Preferences preferences = Preferences.userRoot(); - String downloadFolder = preferences.get(ExploreScreen.PREF_DOWNLOAD_FOLDER, System.getProperty("user.home")); + String downloadFolder = Utils.getDownloadFolder(); JFileChooser chooser = new JFileChooser(); chooser.setCurrentDirectory(new File(downloadFolder)); @@ -282,7 +272,7 @@ private void showDownloadLocation() { if (rc == JFileChooser.APPROVE_OPTION) { File selectedFile = chooser.getSelectedFile(); if (selectedFile != null && selectedFile.exists() && selectedFile.isDirectory()) { - preferences.put(ExploreScreen.PREF_DOWNLOAD_FOLDER, selectedFile.getAbsolutePath()); + PreferenceUtils.setPreference(PreferenceUtils.Pref.PREF_DOWNLOAD_FOLDER, selectedFile.getAbsolutePath()); } } diff --git a/src/main/java/com/jpage4500/devicemanager/utils/PreferenceUtils.java b/src/main/java/com/jpage4500/devicemanager/utils/PreferenceUtils.java new file mode 100644 index 0000000..435dced --- /dev/null +++ b/src/main/java/com/jpage4500/devicemanager/utils/PreferenceUtils.java @@ -0,0 +1,96 @@ +package com.jpage4500.devicemanager.utils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.prefs.Preferences; + +public class PreferenceUtils { + private static final Logger log = LoggerFactory.getLogger(PreferenceUtils.class); + + /** + * String value preferences + */ + public enum Pref { + PREF_CUSTOM_APPS, + PREF_HIDDEN_COLUMNS, + PREF_DOWNLOAD_FOLDER, + PREF_GO_TO_FOLDER_LIST, + PREF_RECENT_WIRELESS_DEVICES, + PREF_LAST_DEVICE_IP, + PREF_CUSTOM_COMMAND_LIST, + } + + /** + * Boolean value preferences + */ + public enum PrefBoolean { + PREF_DEBUG_MODE, + PREF_CHECK_UPDATES, + PREF_ALWAYS_ON_TOP, + PREF_USE_ROOT, + } + + /** + * Boolean value preferences + */ + public enum PrefInt { + PREF_LAST_DEVICE_PORT, + } + + public static String getPreference(Pref pref) { + return getPreference(pref.name()); + } + + public static boolean getPreference(PrefBoolean pref, boolean defaultValue) { + return getPreferenceBool(pref.name(), defaultValue); + } + + public static int getPreference(PrefInt pref, int defaultValue) { + return getPreferenceInt(pref.name(), defaultValue); + } + + public static void setPreference(Pref pref, String value) { + setPreference(pref.name(), value); + } + + public static void setPreference(PrefBoolean key, boolean value) { + setPreference(key.name(), value); + } + + public static void setPreference(PrefInt key, int value) { + setPreference(key.name(), value); + } + + // ------------------------------------------------------------------------ + // ------------------------------------------------------------------------ + + public static String getPreference(String pref) { + return getPreferences().get(pref, null); + } + + public static boolean getPreferenceBool(String pref, boolean defaultValue) { + return getPreferences().getBoolean(pref, defaultValue); + } + + public static int getPreferenceInt(String pref, int defaultValue) { + return getPreferences().getInt(pref, defaultValue); + } + + public static void setPreference(String key, String value) { + getPreferences().put(key, value); + } + + public static void setPreference(String key, boolean value) { + getPreferences().putBoolean(key, value); + } + + public static void setPreference(String key, int value) { + getPreferences().putInt(key, value); + } + + private static Preferences getPreferences() { + return Preferences.userRoot(); + } + +} diff --git a/src/main/java/com/jpage4500/devicemanager/utils/ResultWatcher.java b/src/main/java/com/jpage4500/devicemanager/utils/ResultWatcher.java index f8589b5..e10c015 100644 --- a/src/main/java/com/jpage4500/devicemanager/utils/ResultWatcher.java +++ b/src/main/java/com/jpage4500/devicemanager/utils/ResultWatcher.java @@ -1,5 +1,7 @@ package com.jpage4500.devicemanager.utils; +import com.jpage4500.devicemanager.manager.DeviceManager; + import javax.swing.*; import java.awt.*; @@ -10,11 +12,16 @@ public class ResultWatcher { public StringBuilder sb = new StringBuilder(); private final int numResults; private int counter; + private DeviceManager.TaskListener listener; public ResultWatcher(int numResults) { this.numResults = numResults; } + public void setListener(DeviceManager.TaskListener listener) { + this.listener = listener; + } + public void handleResult(Component component, String line) { SwingUtilities.invokeLater(() -> { if (line != null) sb.append(line); @@ -22,10 +29,12 @@ public void handleResult(Component component, String line) { // show results when last command is complete if (counter == numResults && !sb.isEmpty()) { + // DONE! JTextArea textArea = new JTextArea(sb.toString()); textArea.setEditable(false); JScrollPane scrollPane = new JScrollPane(textArea); JOptionPane.showMessageDialog(component, scrollPane, "Results", JOptionPane.PLAIN_MESSAGE); + if (listener != null) listener.onTaskComplete(true, null); } else { if (line != null) sb.append("\n"); } diff --git a/src/main/java/com/jpage4500/devicemanager/utils/Utils.java b/src/main/java/com/jpage4500/devicemanager/utils/Utils.java index 6a67235..416fb4e 100644 --- a/src/main/java/com/jpage4500/devicemanager/utils/Utils.java +++ b/src/main/java/com/jpage4500/devicemanager/utils/Utils.java @@ -1,5 +1,6 @@ package com.jpage4500.devicemanager.utils; +import com.jpage4500.devicemanager.ui.ExploreScreen; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -9,6 +10,7 @@ import java.io.File; import java.io.IOException; import java.net.URI; +import java.util.prefs.Preferences; public class Utils { private static final Logger log = LoggerFactory.getLogger(Utils.class); @@ -76,6 +78,15 @@ public static boolean editFile(File file) { } } + public static String getDownloadFolder() { + String downloadFolder = PreferenceUtils.getPreference(PreferenceUtils.Pref.PREF_DOWNLOAD_FOLDER); + if (TextUtils.isEmpty(downloadFolder)) { + // default download folder = ~/Downloads + downloadFolder = System.getProperty("user.home") + "/Downloads"; + } + return downloadFolder; + } + public enum CompareResult { VERSION_EQUALS, VERSION_NEWER, From 066ea52b404832546034b1d0d86650c602255704 Mon Sep 17 00:00:00 2001 From: Joe Page Date: Wed, 12 Jun 2024 21:17:06 -0400 Subject: [PATCH 02/11] - adding a message viewer to view log messages that JSON 'pretty' formatting - fix saving table column sizes - more refactoring - add setting to toggle background off --- scripts/clear-preferences.sh | 17 ++ scripts/device-manager.sh | 27 -- .../devicemanager/manager/DeviceManager.java | 280 ++++++++++-------- .../devicemanager/table/DeviceTableModel.java | 21 +- .../devicemanager/ui/DeviceScreen.java | 54 ++-- .../devicemanager/ui/ExploreScreen.java | 1 + .../devicemanager/ui/InputScreen.java | 131 ++++---- .../devicemanager/ui/LogsScreen.java | 102 +++++-- .../devicemanager/ui/MessageViewScreen.java | 148 +++++++++ .../ui/dialog/SettingsDialog.java | 180 +++++------ .../devicemanager/ui/views/CustomFrame.java | 6 +- .../devicemanager/ui/views/CustomTable.java | 80 +++-- .../devicemanager/utils/FileUtils.java | 2 +- .../devicemanager/utils/PreferenceUtils.java | 25 +- .../devicemanager/utils/TextUtils.java | 91 ++++++ .../java/se/vidstige/jadb/JadbDevice.java | 28 +- src/main/resources/images/json.png | Bin 0 -> 9266 bytes src/main/resources/images/logs.png | Bin 0 -> 2516 bytes src/main/resources/images/wrap.png | Bin 0 -> 7426 bytes src/main/resources/images/xml.png | Bin 0 -> 8467 bytes 20 files changed, 744 insertions(+), 449 deletions(-) create mode 100755 scripts/clear-preferences.sh delete mode 100755 scripts/device-manager.sh create mode 100644 src/main/java/com/jpage4500/devicemanager/ui/MessageViewScreen.java create mode 100644 src/main/resources/images/json.png create mode 100644 src/main/resources/images/logs.png create mode 100644 src/main/resources/images/wrap.png create mode 100644 src/main/resources/images/xml.png diff --git a/scripts/clear-preferences.sh b/scripts/clear-preferences.sh new file mode 100755 index 0000000..7366247 --- /dev/null +++ b/scripts/clear-preferences.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +PREFS=com.apple.java.util.prefs.plist +PREFS_FULL=~/Library/Preferences/$PREFS + +# clear out all Preferences (NOTE: only tested on a Mac) + +if [ -f $PREFS_FULL ]; then + echo "removing $PREFS_FULL.." + rm $PREFS_FULL + + USER=$(id -un) + echo "clearing cache for $USER.." + killall -u $USER cfprefsd +else + echo "Preferences not found: $PREFS_FULL" +fi \ No newline at end of file diff --git a/scripts/device-manager.sh b/scripts/device-manager.sh deleted file mode 100755 index 158c9bd..0000000 --- a/scripts/device-manager.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -JAR=AndroidDeviceManager.jar - -# check if app is already running and if so, restart it -PID=$(ps -ef | grep ${JAR} | grep -v grep | tr -s ' ' | cut -d ' ' -f3) -if [ "${PID}" -eq "${PID}" ] 2>/dev/null -then - echo "** running - restarting (${PID}) **" - kill ${PID} -else - echo "not running" -fi - - -# use the latest build version (if found) -if [ -f ~/Downloads/${JAR} ]; then - echo "using latest version.." - cd ~/Downloads -else - # run from script directory - cd "$(dirname $0)" -fi - -java -jar ${JAR} - - diff --git a/src/main/java/com/jpage4500/devicemanager/manager/DeviceManager.java b/src/main/java/com/jpage4500/devicemanager/manager/DeviceManager.java index 8991be0..1785d67 100644 --- a/src/main/java/com/jpage4500/devicemanager/manager/DeviceManager.java +++ b/src/main/java/com/jpage4500/devicemanager/manager/DeviceManager.java @@ -236,15 +236,13 @@ private void fetchDeviceDetails(Device device, boolean fullRefresh, DeviceListen if (!device.isOnline) return; commandExecutorService.submit(() -> { Timer timer = new Timer(); - log.trace("fetchDeviceDetails: {}, full:{}", device.serial, fullRefresh); + // show device as 'busy' device.busyCounter.incrementAndGet(); listener.handleDeviceUpdated(device); if (fullRefresh) { + // -- device nickname -- - List nicknameLines = runShell(device, COMMAND_DEVICE_NICKNAME); - if (!nicknameLines.isEmpty()) { - device.nickname = nicknameLines.get(0).trim(); - } + fetchNickname(device); // -- phone number -- device.phone = runShellServiceCall(device, COMMAND_SERVICE_PHONE1); @@ -260,141 +258,176 @@ private void fetchDeviceDetails(Device device, boolean fullRefresh, DeviceListen } // -- custom properties -- - OutputStream outputStream = new ByteArrayOutputStream(); - RemoteFile file = new RemoteFile(FILE_CUSTOM_PROP); - try { - device.jadbDevice.pull(file, outputStream); - String customPropStr = outputStream.toString(); - String[] customPropArr = customPropStr.split("\\n+"); - for (String customProp : customPropArr) { - String[] propArr = customProp.split("=", 2); - if (propArr.length < 2) continue; - String propKey = propArr[0]; - String propValue = propArr[1]; - // old versions replaced spaces with "~" - propValue = propValue.replaceAll("~", " "); - if (device.customPropertyMap == null) device.customPropertyMap = new HashMap<>(); - device.customPropertyMap.put(propKey, propValue); - } - } catch (Exception e) { - // NOTE: this is normal as file won't exist unless set - //log.trace("fetchDeviceDetails: PULL Exception:{}", e.getMessage()); - } + fetchCustomProperties(device); } // -- disk free space -- - List sizeLines = runShell(device, COMMAND_DISK_SIZE); - if (!sizeLines.isEmpty()) { - // only interested in last line - String last = sizeLines.get(sizeLines.size() - 1); - // /dev/fuse 115249236 14681484 100436680 13% /storage/emulated - // ^^^^^^^^^ - String size = TextUtils.split(last, 3); - try { - // size is in 1k blocks - device.freeSpace = Long.parseLong(size) * 1000L; - } catch (Exception e) { - log.trace("fetchDeviceDetails: FREE_SPACE Exception:{}", e.getMessage()); - } - } + fetchFreeDiskSpace(device); // -- version of installed apps -- - List customApps = SettingsDialog.getCustomApps(); - for (String customApp : customApps) { - // shell dumpsys package $PACKAGE | grep versionName | sed 's/ versionName=//') - List appResultList = runShell(device, "dumpsys package " + customApp); - for (String appLine : appResultList) { - // " versionName=24.05.16.160", - int index = appLine.indexOf("versionName="); - if (index > 0) { - String versionName = appLine.substring(index + "versionName=".length()); - if (device.customAppVersionList == null) device.customAppVersionList = new HashMap<>(); - device.customAppVersionList.put(customApp, versionName); - //log.trace("fetchDeviceDetails: {} = {}", customApp, versionName); - } - } - } + fetchInstalledAppVersions(device); // -- battery level, charging status, etc -- - List batteryList = runShell(device, COMMAND_DUMPSYS_BATTERY); - for (String batteryLine : batteryList) { - String[] batteryArr = batteryLine.split(": ", 2); - if (batteryArr.length < 2) continue; - String name = batteryArr[0].trim(); - String value = batteryArr[1].trim(); - switch (name) { - case "level": - // level: 100 - try { - device.batteryLevel = Integer.parseInt(value); - } catch (NumberFormatException e) { - log.debug("fetchDeviceDetails: BAD_INT: {}, {}", value, e.getMessage()); - } - case "AC powered": - // AC powered: true - if (Boolean.parseBoolean(value)) device.powerStatus = Device.PowerStatus.POWER_AC; - break; - case "USB powered": - // USB powered: false - if (Boolean.parseBoolean(value)) device.powerStatus = Device.PowerStatus.POWER_USB; - break; - case "Wireless powered": - // Wireless powered: false - if (Boolean.parseBoolean(value)) device.powerStatus = Device.PowerStatus.POWER_WIRELESS; - break; - case "Dock powered": - // Dock powered: false - if (Boolean.parseBoolean(value)) device.powerStatus = Device.PowerStatus.POWER_DOCK; - break; - } - } + fetchBatteryInfo(device); + device.lastUpdateMs = System.currentTimeMillis(); if (log.isTraceEnabled()) log.trace("fetchDeviceDetails: {}: full:{}, {}", timer, fullRefresh, GsonHelper.toJson(device)); - int busyCount = device.busyCounter.decrementAndGet(); - if (busyCount == 0) listener.handleDeviceUpdated(device); - if (fullRefresh) { // keep track of wireless devices ConnectDialog.addWirelessDevice(device); } + int busyCount = device.busyCounter.decrementAndGet(); + if (busyCount == 0) listener.handleDeviceUpdated(device); }); } + private void fetchBatteryInfo(Device device) { + ShellResult result = runShell(device, COMMAND_DUMPSYS_BATTERY); + for (String batteryLine : result.resultList) { + String[] batteryArr = batteryLine.split(": ", 2); + if (batteryArr.length < 2) continue; + String name = batteryArr[0].trim(); + String value = batteryArr[1].trim(); + switch (name) { + case "level": + // level: 100 + try { + device.batteryLevel = Integer.parseInt(value); + } catch (NumberFormatException e) { + log.debug("fetchDeviceDetails: BAD_INT: {}, {}", value, e.getMessage()); + } + case "AC powered": + // AC powered: true + if (Boolean.parseBoolean(value)) device.powerStatus = Device.PowerStatus.POWER_AC; + break; + case "USB powered": + // USB powered: false + if (Boolean.parseBoolean(value)) device.powerStatus = Device.PowerStatus.POWER_USB; + break; + case "Wireless powered": + // Wireless powered: false + if (Boolean.parseBoolean(value)) device.powerStatus = Device.PowerStatus.POWER_WIRELESS; + break; + case "Dock powered": + // Dock powered: false + if (Boolean.parseBoolean(value)) device.powerStatus = Device.PowerStatus.POWER_DOCK; + break; + } + } + } + + private void fetchInstalledAppVersions(Device device) { + List customApps = SettingsDialog.getCustomApps(); + for (String customApp : customApps) { + // shell dumpsys package $PACKAGE | grep versionName | sed 's/ versionName=//') + ShellResult result = runShell(device, "dumpsys package " + customApp); + for (String appLine : result.resultList) { + // " versionName=24.05.16.160", + int index = appLine.indexOf("versionName="); + if (index > 0) { + String versionName = appLine.substring(index + "versionName=".length()); + if (device.customAppVersionList == null) device.customAppVersionList = new HashMap<>(); + device.customAppVersionList.put(customApp, versionName); + //log.trace("fetchDeviceDetails: {} = {}", customApp, versionName); + } + } + } + } + + private void fetchFreeDiskSpace(Device device) { + ShellResult result = runShell(device, COMMAND_DISK_SIZE); + if (result.isSuccess && !result.resultList.isEmpty()) { + // only interested in last line + String last = result.resultList.get(result.resultList.size() - 1); + // /dev/fuse 115249236 14681484 100436680 13% /storage/emulated + // ^^^^^^^^^ + String size = TextUtils.split(last, 3); + try { + // size is in 1k blocks + device.freeSpace = Long.parseLong(size) * 1000L; + } catch (Exception e) { + log.trace("fetchDeviceDetails: FREE_SPACE Exception:{}", e.getMessage()); + } + } + } + + private void fetchCustomProperties(Device device) { + OutputStream outputStream = new ByteArrayOutputStream(); + RemoteFile file = new RemoteFile(FILE_CUSTOM_PROP); + try { + device.jadbDevice.pull(file, outputStream); + String customPropStr = outputStream.toString(); + String[] customPropArr = customPropStr.split("\\n+"); + for (String customProp : customPropArr) { + String[] propArr = customProp.split("=", 2); + if (propArr.length < 2) continue; + String propKey = propArr[0]; + String propValue = propArr[1]; + // old versions replaced spaces with "~" + propValue = propValue.replaceAll("~", " "); + if (device.customPropertyMap == null) device.customPropertyMap = new HashMap<>(); + device.customPropertyMap.put(propKey, propValue); + } + } catch (Exception e) { + // NOTE: this is normal as file won't exist unless set + //log.trace("fetchDeviceDetails: PULL Exception:{}", e.getMessage()); + } + } + + private void fetchNickname(Device device) { + ShellResult result = runShell(device, COMMAND_DEVICE_NICKNAME); + if (result.isSuccess && !result.resultList.isEmpty()) { + device.nickname = result.resultList.get(0).trim(); + } + } + /** * run a 'shell service call ..." command and parse the results into a String */ private String runShellServiceCall(Device device, String command) { - List resultList = runShell(device, command); + ShellResult result = runShell(device, command); + if (!result.isSuccess) return null; // Result: Parcel( // 0x00000000: 00000000 0000000b 00350031 00300034 '........1.2.2.2.' // 0x00000010: 00310039 00390034 00310032 00000034 '3.3.3.4.4.4.4...') - StringBuilder result = null; - if (resultList.size() > 1) { - for (int i = 1; i < resultList.size(); i++) { - String line = resultList.get(i); + StringBuilder sb = null; + if (result.resultList.size() > 1) { + for (int i = 1; i < result.resultList.size(); i++) { + String line = result.resultList.get(i); int stPos = line.indexOf('\''); if (stPos >= 0) { int endPos = line.indexOf('\'', stPos + 1); if (endPos >= 0) { line = line.substring(stPos + 1, endPos); line = line.replaceAll("[^-?0-9]+", ""); - if (result == null) result = new StringBuilder(); - result.append(line); + if (sb == null) sb = new StringBuilder(); + sb.append(line); } } } } //log.trace("runShellServiceCall: RESULTS: {}", result); - return result != null ? result.toString() : null; + return sb != null ? sb.toString() : null; + } + + public static class ShellResult { + boolean isSuccess; + List resultList; + + @Override + public String toString() { + return "success: " + isSuccess + ", results: " + GsonHelper.toJson(resultList); + } } /** * run a shell command and return multi-line output */ - private List runShell(Device device, String command) { - List resultList = new ArrayList<>(); + private ShellResult runShell(Device device, String command) { + ShellResult result = new ShellResult(); + result.resultList = new ArrayList<>(); List commandList = TextUtils.splitSafe(command); InputStream inputStream = null; try { @@ -406,10 +439,12 @@ private List runShell(Device device, String command) { String line; while ((line = input.readLine()) != null) { //log.trace("runShell: {}", line); - resultList.add(line); + result.resultList.add(line); } + result.isSuccess = true; } catch (Exception e) { log.error("runShell: cmd:{}, Exception: {}", command, e.getMessage()); + result.isSuccess = false; } finally { if (inputStream != null) { try { @@ -418,7 +453,7 @@ private List runShell(Device device, String command) { } } } - return resultList; + return result; } private Device getDevice(String serial) { @@ -565,10 +600,10 @@ public void restartDevice(Device device, TaskListener listener) { public void runCustomCommand(Device device, String customCommand, TaskListener listener) { commandExecutorService.submit(() -> { - List results = runShell(device, customCommand); - log.debug("runCustomCommand: DONE:{}", GsonHelper.toJson(results)); - String displayStr = TextUtils.join(results, "\n"); - if (listener != null) listener.onTaskComplete(true, displayStr); + ShellResult result = runShell(device, customCommand); + log.debug("runCustomCommand: DONE: success:{}, {}", result.isSuccess, GsonHelper.toJson(result.resultList)); + String displayStr = TextUtils.join(result.resultList, "\n"); + if (listener != null) listener.onTaskComplete(result.isSuccess, displayStr); }); } @@ -602,13 +637,13 @@ public void listFiles(Device device, String path, boolean useRoot, DeviceFileLis if (safePath.indexOf(' ') > 0) { safePath = "\"" + safePath + "\""; } - log.trace("listFiles: {} {}", safePath, useRoot ? "(ROOT)" : ""); + //log.trace("listFiles: {} {}", safePath, useRoot ? "(ROOT)" : ""); String command = "ls -alZ " + safePath; if (useRoot) command = "su -c " + command; - List dirList = runShell(device, command); + ShellResult result = runShell(device, command); List fileList = new ArrayList<>(); - for (int i = 0; i < dirList.size(); i++) { - String dir = dirList.get(i); + for (int i = 0; i < result.resultList.size(); i++) { + String dir = result.resultList.get(i); DeviceFile file = DeviceFile.fromEntry(dir); if (file != null) fileList.add(file); else if (i == 0) { @@ -622,8 +657,6 @@ else if (i == 0) { } else if (TextUtils.containsAny(dir, true, "Not a directory", "No such file or directory")) { listener.handleFiles(null, ERR_NOT_A_DIRECTORY); return; - } else { - log.trace("listFiles: {}", dir); } } } @@ -681,8 +714,8 @@ public void deleteFile(Device device, String path, DeviceFile file, TaskListener commandExecutorService.submit(() -> { String command = "rm -rf " + path + "/" + file.name; if (file.isDirectory) command += "/"; - List resultList = runShell(device, command); - log.debug("deleteFile: {} -> {}", command, GsonHelper.toJson(resultList)); + ShellResult result = runShell(device, command); + log.debug("deleteFile: {} -> {}", command, result); // TODO: determine success/fail listener.onTaskComplete(true, null); }); @@ -690,8 +723,8 @@ public void deleteFile(Device device, String path, DeviceFile file, TaskListener public void createFolder(Device device, String path, TaskListener listener) { commandExecutorService.submit(() -> { - List resultList = runShell(device, "mkdir \"" + path + "\""); - log.debug("createFolder: {} -> {}", path, GsonHelper.toJson(resultList)); + ShellResult result = runShell(device, "mkdir \"" + path + "\""); + log.debug("createFolder: {} -> {}", path, result); // TODO: determine success/fail listener.onTaskComplete(true, null); }); @@ -732,14 +765,11 @@ public void disconnectDevice(String serial, TaskListener listener) { public void sendInputText(Device device, String text, TaskListener listener) { commandExecutorService.submit(() -> { - log.debug("sendInputText: {}", text); - try { - device.jadbDevice.inputText(text); - if (listener != null) listener.onTaskComplete(true, null); - } catch (Exception e) { - log.error("sendInputText: {}, Exception:{}", text, e.getMessage()); - if (listener != null) listener.onTaskComplete(false, null); - } + String command = "input text \"" + text + "\""; + ShellResult result = runShell(device, command); + log.trace("sendInputText: {} -> {}", text, result); + // assume + if (listener != null) listener.onTaskComplete(true, null); }); } @@ -840,11 +870,11 @@ private void scheduleNextProcessCheck(Device device, DeviceLogListener listener) } private Map getProcessMap(Device device) { - List pidList = runShell(device, COMMAND_LIST_PROCESSES); + ShellResult result = runShell(device, COMMAND_LIST_PROCESSES); // 7617 com.android.traceur // 7677 [csf_sync_update] Map pidMap = new HashMap<>(); - for (String line : pidList) { + for (String line : result.resultList) { String[] lineArr = line.trim().split(" ", 2); if (lineArr.length < 2) continue; String pid = lineArr[0]; diff --git a/src/main/java/com/jpage4500/devicemanager/table/DeviceTableModel.java b/src/main/java/com/jpage4500/devicemanager/table/DeviceTableModel.java index 4493b36..1e898cb 100644 --- a/src/main/java/com/jpage4500/devicemanager/table/DeviceTableModel.java +++ b/src/main/java/com/jpage4500/devicemanager/table/DeviceTableModel.java @@ -16,7 +16,6 @@ public class DeviceTableModel extends AbstractTableModel { private final List deviceList; private final List appList; private Columns[] visibleColumns; - private String searchText; public enum Columns { NAME, @@ -64,17 +63,6 @@ public void setHiddenColumns(List hiddenColumns) { fireTableStructureChanged(); } - public void setSearchText(String text) { - if (TextUtils.equals(searchText, text)) return; - searchText = text; - // force re-draw table so we can highlight any matches - fireTableDataChanged(); - } - - public String getSearchText() { - return searchText; - } - /** * get device for given row * NOTE: make sure you use table.convertRowIndexToModel() first @@ -130,14 +118,7 @@ public String getColumnName(int i) { Columns colType = visibleColumns[i]; return colType.name(); } else { - String appName = appList.get(i - visibleColumns.length); - return appName; - //String[] split = appName.split("\\."); - //if (split.length >= 1) { - // return split[split.length - 1].toUpperCase(Locale.ROOT); - //} else { - // return "?"; - //} + return appList.get(i - visibleColumns.length); } } diff --git a/src/main/java/com/jpage4500/devicemanager/ui/DeviceScreen.java b/src/main/java/com/jpage4500/devicemanager/ui/DeviceScreen.java index 643c5ad..f741275 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/DeviceScreen.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/DeviceScreen.java @@ -171,6 +171,9 @@ private void setupMenuBar() { // [CMD + 3] = show logs createCmdAction(windowMenu, "View Logs", KeyEvent.VK_3, e -> handleLogsCommand()); + // [CMD + ,] = settings + createCmdAction(windowMenu, "Settings", KeyEvent.VK_COMMA, e -> handleSettingsClicked()); + // [CMD + T] = hide toolbar createCmdAction(windowMenu, "Hide Toolbar", KeyEvent.VK_T, e -> hideToolbar()); @@ -222,19 +225,19 @@ private void setupTable() { table.setDefaultRenderer(Device.class, new DeviceCellRenderer()); table.setEmptyText("No Android Devices!"); - // default column sizes - TableColumnModel columnModel = table.getColumnModel(); - columnModel.getColumn(DeviceTableModel.Columns.NAME.ordinal()).setPreferredWidth(185); - columnModel.getColumn(DeviceTableModel.Columns.SERIAL.ordinal()).setPreferredWidth(152); - columnModel.getColumn(DeviceTableModel.Columns.PHONE.ordinal()).setPreferredWidth(116); - columnModel.getColumn(DeviceTableModel.Columns.IMEI.ordinal()).setPreferredWidth(147); - columnModel.getColumn(DeviceTableModel.Columns.BATTERY.ordinal()).setPreferredWidth(31); - columnModel.getColumn(DeviceTableModel.Columns.BATTERY.ordinal()).setMaxWidth(31); - columnModel.getColumn(DeviceTableModel.Columns.FREE.ordinal()).setPreferredWidth(66); - columnModel.getColumn(DeviceTableModel.Columns.FREE.ordinal()).setMaxWidth(80); - // restore user-defined column sizes - table.restore(); + if (!table.restore()) { + // first-time running - use some default column sizes + TableColumnModel columnModel = table.getColumnModel(); + columnModel.getColumn(DeviceTableModel.Columns.NAME.ordinal()).setPreferredWidth(185); + columnModel.getColumn(DeviceTableModel.Columns.SERIAL.ordinal()).setPreferredWidth(152); + columnModel.getColumn(DeviceTableModel.Columns.PHONE.ordinal()).setPreferredWidth(116); + columnModel.getColumn(DeviceTableModel.Columns.IMEI.ordinal()).setPreferredWidth(147); + columnModel.getColumn(DeviceTableModel.Columns.BATTERY.ordinal()).setPreferredWidth(31); + columnModel.getColumn(DeviceTableModel.Columns.BATTERY.ordinal()).setMaxWidth(31); + columnModel.getColumn(DeviceTableModel.Columns.FREE.ordinal()).setPreferredWidth(66); + columnModel.getColumn(DeviceTableModel.Columns.FREE.ordinal()).setMaxWidth(80); + } sorter = new DeviceRowSorter(model); table.setRowSorter(sorter); @@ -397,14 +400,13 @@ private void bringWindowToFront() { private void updateDeviceState(Device device) { ExploreScreen exploreScreen = exploreViewMap.get(device.serial); - if (exploreScreen != null) { - exploreScreen.updateDeviceState(); - } + if (exploreScreen != null) exploreScreen.updateDeviceState(); LogsScreen logsScreen = logsViewMap.get(device.serial); - if (logsScreen != null) { - logsScreen.updateDeviceState(); - } + if (logsScreen != null) logsScreen.updateDeviceState(); + + InputScreen inputScreen = inputViewMap.get(device.serial); + if (inputScreen != null) inputScreen.updateDeviceState(); } private void refreshUi() { @@ -445,7 +447,9 @@ private void handleHideColumn(int column) { List hiddenColList = SettingsDialog.getHiddenColumnList(); hiddenColList.add(columnType.name()); PreferenceUtils.setPreference(PreferenceUtils.Pref.PREF_HIDDEN_COLUMNS, GsonHelper.toJson(hiddenColList)); + table.persist(); model.setHiddenColumns(hiddenColList); + table.restore(); } private void handleCopyClipboardFieldCommand() { @@ -752,7 +756,7 @@ private List getSelectedDevices() { // convert view row to data row (in case user changed sort order) int dataRow = table.convertRowIndexToModel(selectedRow); Device device = model.getDeviceAtRow(dataRow); - if (device != null) selectedDeviceList.add(device); + if (device != null && device.isOnline) selectedDeviceList.add(device); } if (selectedDeviceList.isEmpty() && model.getRowCount() == 1) { Device device = model.getDeviceAtRow(0); @@ -770,7 +774,7 @@ private void setupToolbar() { createToolbarButton(toolbar, "icon_browse.png", "Browse", "File Explorer", actionEvent -> handleBrowseCommand()); - createToolbarButton(toolbar, "icon_edit.png", "Logs", "Log Viewer", actionEvent -> handleLogsCommand()); + createToolbarButton(toolbar, "logs.png", "Logs", "Log Viewer", actionEvent -> handleLogsCommand()); createToolbarButton(toolbar, "keyboard.png", "Input", "Enter text", actionEvent -> handleInputCommand()); @@ -803,7 +807,7 @@ private void setupToolbar() { } private void handleSettingsClicked() { - SettingsDialog.showSettings(this, model); + SettingsDialog.showSettings(this); } private void refreshDevices() { @@ -891,6 +895,14 @@ public void handleBrowseClosed(String serial) { exploreViewMap.remove(serial); } + public void handleLogsClosed(String serial) { + logsViewMap.remove(serial); + } + + public void handleInputClosed(String serial) { + inputViewMap.remove(serial); + } + public void handleLogsCommand() { Device selectedDevice = getFirstSelectedDevice(); if (selectedDevice == null) return; diff --git a/src/main/java/com/jpage4500/devicemanager/ui/ExploreScreen.java b/src/main/java/com/jpage4500/devicemanager/ui/ExploreScreen.java index a37f5c1..34b97f9 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/ExploreScreen.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/ExploreScreen.java @@ -179,6 +179,7 @@ private void setupTable() { // restore user-defined column sizes table.restore(); + // ENTER -> click on file KeyStroke enter = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0); table.getInputMap(JTable.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(enter, "Enter"); table.getActionMap().put("Enter", new AbstractAction() { diff --git a/src/main/java/com/jpage4500/devicemanager/ui/InputScreen.java b/src/main/java/com/jpage4500/devicemanager/ui/InputScreen.java index cfef3d9..d2a8016 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/InputScreen.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/InputScreen.java @@ -3,113 +3,70 @@ import com.jpage4500.devicemanager.data.Device; import com.jpage4500.devicemanager.manager.DeviceManager; import com.jpage4500.devicemanager.table.utils.AlternatingBackgroundColorRenderer; -import com.jpage4500.devicemanager.ui.views.CustomFrame; import com.jpage4500.devicemanager.utils.GsonHelper; +import com.jpage4500.devicemanager.utils.PreferenceUtils; import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.swing.*; import java.awt.*; -import java.awt.event.*; +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; import java.util.List; -import java.util.prefs.Preferences; /** * dialog to enter text on device */ -public class InputScreen { +public class InputScreen extends BaseScreen { private static final Logger log = LoggerFactory.getLogger(InputScreen.class); - private JFrame deviceFrame; - public CustomFrame frame; - public JPanel panel; + private final DeviceScreen deviceScreen; + private Device device; private JTextField textField; private DefaultListModel listModel; - private Device selectedDevice; + public InputScreen(DeviceScreen deviceScreen, Device device) { + super("input"); + this.deviceScreen = deviceScreen; + this.device = device; + setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); - public InputScreen(JFrame deviceFrame, Device selectedDevice) { - this.deviceFrame = deviceFrame; - this.selectedDevice = selectedDevice; initalizeUi(); - - frame.setTitle(selectedDevice.getDisplayName()); + updateDeviceState(); } - public void show() { - frame.setVisible(true); - textField.requestFocus(); + public void updateDeviceState() { + if (device.isOnline) { + setTitle("Input [" + device.getDisplayName() + "]"); + } else { + setTitle("OFFLINE [" + device.getDisplayName() + "]"); + } } private void initalizeUi() { - frame = new CustomFrame("input"); - panel = new JPanel(); - panel.setLayout(new BorderLayout()); - frame.addWindowListener(new WindowAdapter() { - @Override - public void windowActivated(WindowEvent e) { - - } + JPanel panel = new JPanel(new BorderLayout()); + panel.setLayout(new MigLayout("fillx", "[][]")); - @Override - public void windowDeactivated(WindowEvent e) { - } - }); - frame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE); - frame.addWindowListener(new java.awt.event.WindowAdapter() { + addWindowListener(new java.awt.event.WindowAdapter() { @Override public void windowClosing(java.awt.event.WindowEvent windowEvent) { - + closeWindow(); } }); - // -- CMD+W = close window -- - Action closeAction = new AbstractAction("Close Window") { - @Override - public void actionPerformed(ActionEvent e) { - log.debug("actionPerformed: CLOSE"); - frame.setVisible(false); - frame.dispose(); - } - }; - - int mask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(); - KeyStroke closeKey = KeyStroke.getKeyStroke(KeyEvent.VK_W, mask); - closeAction.putValue(Action.ACCELERATOR_KEY, closeKey); - - // -- CMD+~ = show devices -- - Action switchAction = new AbstractAction("Show Devices") { - @Override - public void actionPerformed(ActionEvent e) { - deviceFrame.toFront(); - } - }; - KeyStroke switchKey = KeyStroke.getKeyStroke(KeyEvent.VK_1, mask); - switchAction.putValue(Action.ACCELERATOR_KEY, switchKey); - - JMenuBar menubar = new JMenuBar(); - JMenu menu = new JMenu("Window"); - JMenuItem closeItem = new JMenuItem("Close"); - closeItem.setAction(closeAction); - menu.add(closeItem); - JMenuItem switchItem = new JMenuItem("Show Devices"); - switchItem.setAction(switchAction); - menu.add(switchItem); - menubar.add(menu); - frame.setJMenuBar(menubar); - - panel.setLayout(new MigLayout("fillx", "[][]")); + setupMenuBar(); panel.add(new JLabel("Recent Text"), "growx, span 2, wrap"); - Preferences preferences = Preferences.userRoot(); - String recentInput = preferences.get("PREF_RECENT_INPUT", null); + String recentInput = PreferenceUtils.getPreference(PreferenceUtils.Pref.PREF_RECENT_INPUT); List recentInputList = GsonHelper.stringToList(recentInput, String.class); listModel = new DefaultListModel<>(); - //listModel.addAll(recentInputList); + listModel.addAll(recentInputList); JList list = new JList<>(listModel); list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); @@ -127,7 +84,7 @@ public void focusLost(FocusEvent e) { panel.add(new JSeparator(), "growx, spanx, wrap"); - panel.add(new JLabel("Input"), "growx, span 2, wrap"); + panel.add(new JLabel("Enter Text"), "growx, span 2, wrap"); textField = new JTextField(); textField.setHorizontalAlignment(SwingConstants.RIGHT); @@ -146,7 +103,7 @@ public void keyPressed(KeyEvent e) { int selectedIndex = list.getSelectedIndex(); if (selectedIndex == -1) return; String value = list.getSelectedValue(); - // TODO + textField.setText(value); }); panel.add(textField, "growx, span 2, wrap"); @@ -155,7 +112,30 @@ public void keyPressed(KeyEvent e) { sendButton.addActionListener(e -> handleEnterPressed()); panel.add(sendButton, "al right, span 2, wrap"); - frame.setContentPane(panel); + setContentPane(panel); + } + + private void setupMenuBar() { + JMenu windowMenu = new JMenu("Window"); + + // [CMD + W] = close window + createCmdAction(windowMenu, "Close Window", KeyEvent.VK_W, e -> closeWindow()); + + // [CMD + 1] = show devices + createCmdAction(windowMenu, "Show Devices", KeyEvent.VK_1, e -> deviceScreen.toFront()); + + // [CMD + 3] = show logs + createCmdAction(windowMenu, "View Logs", KeyEvent.VK_3, e -> deviceScreen.handleLogsCommand()); + + JMenuBar menubar = new JMenuBar(); + menubar.add(windowMenu); + setJMenuBar(menubar); + } + + private void closeWindow() { + log.trace("closeWindow: {}", device.getDisplayName()); + deviceScreen.handleInputClosed(device.serial); + dispose(); } private void handleEnterPressed() { @@ -163,7 +143,7 @@ private void handleEnterPressed() { textField.setEnabled(false); if (text.isEmpty()) { // send newline character - DeviceManager.getInstance().sendInputKeyCode(selectedDevice, 66, (isSuccess, error) -> { + DeviceManager.getInstance().sendInputKeyCode(device, 66, (isSuccess, error) -> { textField.setEnabled(true); if (isSuccess) { // clear out text @@ -176,7 +156,7 @@ private void handleEnterPressed() { return; } - DeviceManager.getInstance().sendInputText(selectedDevice, text, (isSuccess, error) -> { + DeviceManager.getInstance().sendInputText(device, text, (isSuccess, error) -> { textField.setEnabled(true); if (isSuccess) { // clear out text @@ -192,6 +172,5 @@ private void handleEnterPressed() { } - } diff --git a/src/main/java/com/jpage4500/devicemanager/ui/LogsScreen.java b/src/main/java/com/jpage4500/devicemanager/ui/LogsScreen.java index bc51d81..6fdf27f 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/LogsScreen.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/LogsScreen.java @@ -23,6 +23,7 @@ import javax.swing.table.TableColumnModel; import java.awt.*; import java.awt.event.ActionEvent; +import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.util.ArrayList; import java.util.List; @@ -53,6 +54,7 @@ public class LogsScreen extends BaseScreen implements DeviceManager.DeviceLogLis private JList filterList; private LogsRowSorter sorter; + private MessageViewScreen viewScreen; public JButton logButton; public boolean isLoggedPaused; // true when user clicks on 'stop logging' @@ -61,7 +63,7 @@ public LogsScreen(DeviceScreen deviceScreen, Device device) { super("logs"); this.deviceScreen = deviceScreen; this.device = device; - //setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); initalizeUi(); updateDeviceState(); } @@ -158,10 +160,7 @@ private void setupMenuBar() { JMenu windowMenu = new JMenu("Window"); // [CMD + W] = close window - createCmdAction(windowMenu, "Close Window", KeyEvent.VK_W, e -> { - setVisible(false); - dispose(); - }); + createCmdAction(windowMenu, "Close Window", KeyEvent.VK_W, e -> closeWindow()); // [CMD + 1] = show devices createCmdAction(windowMenu, "Show Devices", KeyEvent.VK_1, e -> deviceScreen.toFront()); @@ -207,6 +206,13 @@ private void setupMenuBar() { setJMenuBar(menubar); } + private void closeWindow() { + log.trace("closeWindow: {}", device.getDisplayName()); + //stopLogging(); + deviceScreen.handleLogsClosed(device.serial); + dispose(); + } + private void hideToolbar() { toolbar.setVisible(!toolbar.isVisible()); } @@ -217,17 +223,37 @@ private void setupTable() { table.setModel(model); table.setDefaultRenderer(LogEntry.class, new LogsCellRenderer()); - // default column sizes - TableColumnModel columnModel = table.getColumnModel(); - columnModel.getColumn(LogsTableModel.Columns.LEVEL.ordinal()).setPreferredWidth(28); - columnModel.getColumn(LogsTableModel.Columns.PID.ordinal()).setPreferredWidth(60); - columnModel.getColumn(LogsTableModel.Columns.TID.ordinal()).setPreferredWidth(60); - columnModel.getColumn(LogsTableModel.Columns.DATE.ordinal()).setPreferredWidth(159); - columnModel.getColumn(LogsTableModel.Columns.APP.ordinal()).setPreferredWidth(150); - columnModel.getColumn(LogsTableModel.Columns.MSG.ordinal()).setPreferredWidth(700); - // restore user-defined column sizes - table.restore(); + if (!table.restore()) { + // default column sizes + TableColumnModel columnModel = table.getColumnModel(); + columnModel.getColumn(LogsTableModel.Columns.LEVEL.ordinal()).setPreferredWidth(28); + columnModel.getColumn(LogsTableModel.Columns.PID.ordinal()).setPreferredWidth(60); + columnModel.getColumn(LogsTableModel.Columns.TID.ordinal()).setPreferredWidth(60); + columnModel.getColumn(LogsTableModel.Columns.DATE.ordinal()).setPreferredWidth(159); + columnModel.getColumn(LogsTableModel.Columns.APP.ordinal()).setPreferredWidth(150); + columnModel.getColumn(LogsTableModel.Columns.MSG.ordinal()).setPreferredWidth(700); + } + + // ENTER -> view message + KeyStroke enter = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0); + table.getInputMap(JTable.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(enter, "Enter"); + table.getActionMap().put("Enter", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + handleLogClicked(); + } + }); + + // CMD+SHIFT+V -> view message + KeyStroke view = KeyStroke.getKeyStroke(KeyEvent.VK_V, InputEvent.META_DOWN_MASK + InputEvent.SHIFT_DOWN_MASK); + table.getInputMap(JTable.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(view, "View"); + table.getActionMap().put("View", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + handleLogClicked(); + } + }); table.getSelectionModel().addListSelectionListener(event -> { if (event.getValueIsAdjusting()) return; @@ -241,14 +267,15 @@ private void setupTable() { }); table.setPopupMenuListener((row, column) -> { + JPopupMenu popupMenu = null; if (row == -1) { - JPopupMenu popupMenu = new JPopupMenu(); JMenuItem sizeToFitItem = new JMenuItem("Size to Fit"); sizeToFitItem.addActionListener(actionEvent -> { TableColumnAdjuster adjuster = new TableColumnAdjuster(table, 0); int tableCol = table.convertColumnIndexToView(column); adjuster.adjustColumn(tableCol); }); + popupMenu = new JPopupMenu(); popupMenu.add(sizeToFitItem); return popupMenu; } @@ -262,19 +289,30 @@ private void setupTable() { case TAG: // filter by value String text = model.getTextValue(row, column); - JPopupMenu popupMenu = new JPopupMenu(); JMenuItem copyFieldItem = new JMenuItem("Add Filter"); copyFieldItem.addActionListener(actionEvent -> handleAddFilter(columnType, text)); + popupMenu = new JPopupMenu(); popupMenu.add(copyFieldItem); - break; } - return null; + // view message + JMenuItem viewItem = new JMenuItem("View Message"); + viewItem.addActionListener(actionEvent -> handleLogClicked()); + if (popupMenu == null) popupMenu = new JPopupMenu(); + popupMenu.add(viewItem); + + return popupMenu; }); sorter = new LogsRowSorter(model); table.setRowSorter(sorter); + table.setClickListener((row, column, e) -> { + LogEntry logEntry = (LogEntry) model.getValueAt(row, column); + if (logEntry == null) return; + viewMessage(logEntry); + }); + table.getScrollPane().addMouseWheelListener(event -> { int wheelRotation = event.getWheelRotation(); if (wheelRotation == -1) { @@ -299,6 +337,25 @@ private void setupTable() { }); } + private void handleLogClicked() { + List logEntryList = new ArrayList<>(); + int[] selectedRows = table.getSelectedRows(); + for (int selectedRow : selectedRows) { + int realRow = table.convertRowIndexToModel(selectedRow); + LogEntry logEntry = (LogEntry) model.getValueAt(realRow, 0); + logEntryList.add(logEntry); + } + if (!logEntryList.isEmpty()) { + viewMessage(logEntryList.toArray(new LogEntry[0])); + } + } + + private void viewMessage(LogEntry... logEntry) { + if (viewScreen == null) viewScreen = new MessageViewScreen(); + viewScreen.setLogEntry(logEntry); + viewScreen.setVisible(true); + } + private void handleAddFilter(LogsTableModel.Columns columnType, String text) { LogFilter filter = LogFilter.parse(columnType.name().toLowerCase() + ":" + text); filterField.setText(filter.toString()); @@ -433,7 +490,12 @@ private void filterDevices(String text) { } if (TextUtils.notEmpty(text)) { - LogFilter searchFilter = LogFilter.parse("*:*" + text + "*"); + LogFilter searchFilter; + if (TextUtils.indexOf(text, ':') >= 0) { + searchFilter = LogFilter.parse(text); + } else { + searchFilter = LogFilter.parse("*:*" + text + "*"); + } log.debug("filterDevices: {}", searchFilter); list.add(searchFilter); if (!sb.isEmpty()) sb.append(" && "); diff --git a/src/main/java/com/jpage4500/devicemanager/ui/MessageViewScreen.java b/src/main/java/com/jpage4500/devicemanager/ui/MessageViewScreen.java new file mode 100644 index 0000000..9e8dc25 --- /dev/null +++ b/src/main/java/com/jpage4500/devicemanager/ui/MessageViewScreen.java @@ -0,0 +1,148 @@ +package com.jpage4500.devicemanager.ui; + +import com.jpage4500.devicemanager.data.LogEntry; +import com.jpage4500.devicemanager.utils.PreferenceUtils; +import com.jpage4500.devicemanager.utils.TextUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.KeyEvent; + +/** + * view log messages + */ +public class MessageViewScreen extends BaseScreen { + private static final Logger log = LoggerFactory.getLogger(MessageViewScreen.class); + public static final String TEXT_FORMAT_JSON = "Format JSON"; + public static final String TEXT_RESTORE = "Restore"; + public static final String TEXT_FORMAT_XML = "Format XML"; + public static final String TEXT_WRAP_ON = "Wrap ON"; + public static final String TEXT_AUTO_FORMAT_ON = "Auto Format ON"; + public static final String TEXT_AUTO_FORMAT_OFF = "Auto Format OFF"; + + private LogEntry[] logEntryArr; + + private JTextArea textArea; + private JButton jsonButton; + private JButton xmlButton; + private JButton wrapButton; + private JButton autoFormatButton; + + public MessageViewScreen() { + super("message"); + setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + initalizeUi(); + } + + protected void initalizeUi() { + JPanel mainPanel = new JPanel(new BorderLayout()); + + // -- toolbar -- + JToolBar toolbar = new JToolBar(); + setupToolbar(toolbar); + mainPanel.add(toolbar, BorderLayout.NORTH); + + // -- message -- + textArea = new JTextArea(); + textArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); + + JScrollPane scrollPane = new JScrollPane(textArea); + mainPanel.add(scrollPane, BorderLayout.CENTER); + + setupMenuBar(); + + setTitle("Message Viewer"); + + setContentPane(mainPanel); + setVisible(true); + } + + private void setupToolbar(JToolBar toolbar) { + //toolbar.add(Box.createHorizontalGlue()); + + jsonButton = createToolbarButton(toolbar, "json.png", TEXT_FORMAT_JSON, "Format JSON", actionEvent -> toggleJson()); + xmlButton = createToolbarButton(toolbar, "xml.png", TEXT_FORMAT_XML, "Format XML", actionEvent -> formatXml()); + wrapButton = createToolbarButton(toolbar, "wrap.png", TEXT_WRAP_ON, "Wrap ON", actionEvent -> toggleWrap()); + autoFormatButton = createToolbarButton(toolbar, null, TEXT_AUTO_FORMAT_ON, "Auto Format ON", actionEvent -> toggleAutoFormat()); + } + + private void setupMenuBar() { + JMenu windowMenu = new JMenu("Window"); + + // [CMD + W] = close window + createCmdAction(windowMenu, "Close Window", KeyEvent.VK_W, e -> closeWindow()); + + JMenuBar menubar = new JMenuBar(); + menubar.add(windowMenu); + setJMenuBar(menubar); + } + + private void closeWindow() { + log.trace("closeWindow:"); + dispose(); + } + + public void setLogEntry(LogEntry... logEntryArr) { + this.logEntryArr = logEntryArr; + + boolean autoFormat = PreferenceUtils.getPreference(PreferenceUtils.PrefBoolean.PREF_AUTO_FORMAT_MESSAGE, true); + if (autoFormat) { + formatJson(); + } else { + restoreText(); + } + } + + private String getLogText() { + StringBuilder msg = new StringBuilder(); + for (LogEntry logEntry : logEntryArr) { + if (!msg.isEmpty()) msg.append("\n"); + msg.append(logEntry.message); + } + return msg.toString(); + } + + private void toggleAutoFormat() { + boolean autoFormat = !PreferenceUtils.getPreference(PreferenceUtils.PrefBoolean.PREF_AUTO_FORMAT_MESSAGE, true); + autoFormatButton.setText(autoFormat ? TEXT_AUTO_FORMAT_ON : TEXT_AUTO_FORMAT_OFF); + } + + private void formatXml() { + + } + + private void toggleWrap() { + boolean lineWrap = !textArea.getLineWrap(); + textArea.setLineWrap(lineWrap); + textArea.setWrapStyleWord(lineWrap); + } + + private void toggleJson() { + String currentText = jsonButton.getText(); + switch (currentText) { + case TEXT_FORMAT_JSON: + // format JSON + formatJson(); + break; + case TEXT_RESTORE: + // restore original text + restoreText(); + break; + } + } + + private void formatJson() { + String prettyText = TextUtils.formatJson(getLogText()); + textArea.setText(prettyText); + jsonButton.setText(TEXT_RESTORE); + } + + private void restoreText() { + // restore original text + textArea.setText(getLogText()); + jsonButton.setText(TEXT_FORMAT_JSON); + } + +} diff --git a/src/main/java/com/jpage4500/devicemanager/ui/dialog/SettingsDialog.java b/src/main/java/com/jpage4500/devicemanager/ui/dialog/SettingsDialog.java index 9a31683..74ab22a 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/dialog/SettingsDialog.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/dialog/SettingsDialog.java @@ -3,6 +3,7 @@ import com.jpage4500.devicemanager.logging.AppLoggerFactory; import com.jpage4500.devicemanager.logging.Log; import com.jpage4500.devicemanager.table.DeviceTableModel; +import com.jpage4500.devicemanager.ui.DeviceScreen; import com.jpage4500.devicemanager.ui.views.CheckBoxList; import com.jpage4500.devicemanager.utils.GsonHelper; import com.jpage4500.devicemanager.utils.PreferenceUtils; @@ -12,126 +13,98 @@ import org.slf4j.LoggerFactory; import javax.swing.*; -import java.awt.*; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; -import java.awt.font.TextAttribute; import java.io.File; import java.util.ArrayList; import java.util.List; -import java.util.Map; -import java.util.prefs.BackingStoreException; -import java.util.prefs.Preferences; public class SettingsDialog extends JPanel { private static final Logger log = LoggerFactory.getLogger(SettingsDialog.class); - private Component frame; - private DeviceTableModel tableModel; + private DeviceScreen deviceScreen; - private JCheckBox debugCheckbox; - private JLabel viewLogsLabel; - private JLabel resetLabel; - - public static int showSettings(Component frame, DeviceTableModel tableModel) { - SettingsDialog settingsScreen = new SettingsDialog(frame, tableModel); - return JOptionPane.showOptionDialog(frame, settingsScreen, "Settings", JOptionPane.DEFAULT_OPTION, JOptionPane.PLAIN_MESSAGE, null, null, null); + public static int showSettings(DeviceScreen deviceScreen) { + SettingsDialog settingsScreen = new SettingsDialog(deviceScreen); + return JOptionPane.showOptionDialog(deviceScreen, settingsScreen, "Settings", JOptionPane.DEFAULT_OPTION, JOptionPane.PLAIN_MESSAGE, null, null, null); } - private SettingsDialog(Component frame, DeviceTableModel tableModel) { - this.frame = frame; - this.tableModel = tableModel; + private SettingsDialog(DeviceScreen deviceScreen) { + this.deviceScreen = deviceScreen; setLayout(new MigLayout("", "[][]")); + initalizeUi(); + } - // columns - add(new JLabel("Columns:")); - JButton colsButton = new JButton("EDIT"); - colsButton.addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - showColumns(); - } - }); - add(colsButton, "wrap"); + private void initalizeUi() { + addButton("Visible Columns", "EDIT", this::showColumns); + addButton("Custom Apps", "EDIT", this::showAppsSettings); + addButton("Download Location", "EDIT", this::showDownloadLocation); - // custom apps - add(new JLabel("Custom Apps:")); - JButton appButton = new JButton("EDIT"); - appButton.addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - showAppsSettings(); - } + addCheckbox("Check for updates", PreferenceUtils.PrefBoolean.PREF_CHECK_UPDATES, true, null); + addCheckbox("Show background image", PreferenceUtils.PrefBoolean.PREF_SHOW_BACKGROUND, true, isChecked -> { + // force table background to be repainted + deviceScreen.model.fireTableDataChanged(); }); - add(appButton, "wrap"); - - // download location - add(new JLabel("Download Location:")); - appButton = new JButton("EDIT"); - appButton.addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - showDownloadLocation(); - } + addCheckbox("Debug Mode", PreferenceUtils.PrefBoolean.PREF_DEBUG_MODE, false, isChecked -> { + AppLoggerFactory logger = (AppLoggerFactory) LoggerFactory.getILoggerFactory(); + logger.setFileLogLevel(isChecked ? Log.DEBUG : Log.INFO); }); - add(appButton, "wrap"); - JCheckBox updateCheckbox = new JCheckBox("Check for updates"); - boolean checkUpdates = PreferenceUtils.getPreference(PreferenceUtils.PrefBoolean.PREF_CHECK_UPDATES, true); - updateCheckbox.setSelected(checkUpdates); - updateCheckbox.setHorizontalTextPosition(SwingConstants.LEFT); - updateCheckbox.addMouseListener(new MouseAdapter() { + addButton("View Logs", "VIEW", this::viewLogs); + addButton("Reset Preferences", "RESET", this::resetPreferences); + + doLayout(); + invalidate(); + } + + public interface ButtonListener { + void onClicked(); + } + + private void addButton(String label, String action, ButtonListener listener) { + add(new JLabel(label)); + JButton button = new JButton(action); + button.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { - PreferenceUtils.setPreference(PreferenceUtils.PrefBoolean.PREF_CHECK_UPDATES, updateCheckbox.isSelected()); + listener.onClicked(); } }); - add(updateCheckbox, "span 2, al right, wrap"); + add(button, "wrap"); + } - add(new JSeparator(), "growx, spanx, wrap"); + public interface CheckBoxListener { + void onChecked(boolean isChecked); + } - boolean isDebugMode = PreferenceUtils.getPreference(PreferenceUtils.PrefBoolean.PREF_DEBUG_MODE, false); - debugCheckbox = new JCheckBox("Debug Mode"); - debugCheckbox.setHorizontalTextPosition(SwingConstants.LEFT); - debugCheckbox.setSelected(isDebugMode); - debugCheckbox.addChangeListener(e -> { - boolean updatedValue = debugCheckbox.isSelected(); - PreferenceUtils.setPreference(PreferenceUtils.PrefBoolean.PREF_DEBUG_MODE, updatedValue); - AppLoggerFactory logger = (AppLoggerFactory) LoggerFactory.getILoggerFactory(); - logger.setFileLogLevel(updatedValue ? Log.DEBUG : Log.INFO); - refreshUi(); - }); - add(debugCheckbox, "span 2, al right, wrap"); - - viewLogsLabel = new JLabel("View Logs"); - viewLogsLabel.setForeground(Color.BLUE); - Font font = viewLogsLabel.getFont(); - Map attributes = font.getAttributes(); - attributes.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON); - viewLogsLabel.setFont(font.deriveFont(attributes)); - viewLogsLabel.setHorizontalTextPosition(SwingConstants.LEFT); - viewLogsLabel.addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - viewLogs(); - } + private void addCheckbox(String label, PreferenceUtils.PrefBoolean pref, boolean defaultValue, CheckBoxListener listener) { + JLabel textLabel = new JLabel(label); + add(textLabel); + + JCheckBox checkbox = new JCheckBox(); + boolean currentChecked = PreferenceUtils.getPreference(pref, defaultValue); + checkbox.setSelected(currentChecked); + checkbox.setHorizontalTextPosition(SwingConstants.LEFT); + add(checkbox, "align center, wrap"); + + checkbox.addActionListener(actionEvent -> { + boolean selected = checkbox.isSelected(); + PreferenceUtils.setPreference(pref, selected); + if (listener != null) listener.onChecked(selected); }); - add(viewLogsLabel, "span 2, al right, wrap"); - resetLabel = new JLabel("Reset Preferences"); - resetLabel.setForeground(Color.BLUE); - resetLabel.setFont(font.deriveFont(attributes)); - resetLabel.setHorizontalTextPosition(SwingConstants.LEFT); - resetLabel.addMouseListener(new MouseAdapter() { + textLabel.addMouseListener(new MouseAdapter() { @Override - public void mouseClicked(MouseEvent e) { - resetPreferences(); + public void mouseClicked(MouseEvent mouseEvent) { + // TODO: fire checkbox action listener directly + boolean selected = !checkbox.isSelected(); + checkbox.setSelected(selected); + PreferenceUtils.setPreference(pref, selected); + if (listener != null) listener.onChecked(selected); } }); - add(resetLabel, "span 2, al right, wrap"); - refreshUi(); } private void resetPreferences() { @@ -139,17 +112,12 @@ private void resetPreferences() { if (rc != JOptionPane.YES_OPTION) return; log.debug("resetPreferences: "); - Preferences preferences = Preferences.userRoot(); - try { - preferences.clear(); - } catch (BackingStoreException e) { - } - - } + PreferenceUtils.resetAll(); - private void refreshUi() { - boolean isDebugMode = debugCheckbox.isSelected(); - viewLogsLabel.setVisible(isDebugMode); + removeAll(); + initalizeUi(); + // force table background to be repainted + deviceScreen.model.fireTableDataChanged(); } private void viewLogs() { @@ -159,7 +127,7 @@ private void viewLogs() { boolean rc = Utils.editFile(logsFile); if (!rc) { // open failed - JOptionPane.showConfirmDialog(frame, "Failed to open logs: " + logsFile.getAbsolutePath(), "Error", JOptionPane.DEFAULT_OPTION); + JOptionPane.showConfirmDialog(deviceScreen, "Failed to open logs: " + logsFile.getAbsolutePath(), "Error", JOptionPane.DEFAULT_OPTION); } } @@ -184,14 +152,16 @@ private void showColumns() { JScrollPane scroll = new JScrollPane(checkBoxList); panel.add(scroll, "grow, span, wrap"); - int rc = JOptionPane.showOptionDialog(frame, panel, "Visible Columns", JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE, null, null, null); + int rc = JOptionPane.showOptionDialog(deviceScreen, panel, "Visible Columns", JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE, null, null, null); if (rc != JOptionPane.YES_OPTION) return; // save columns that are NOT selected List selectedItems = checkBoxList.getUnSelectedItems(); log.debug("HIDDEN: {}", GsonHelper.toJson(selectedItems)); PreferenceUtils.setPreference(PreferenceUtils.Pref.PREF_HIDDEN_COLUMNS, GsonHelper.toJson(selectedItems)); - tableModel.setHiddenColumns(selectedItems); + deviceScreen.table.persist(); + deviceScreen.model.setHiddenColumns(selectedItems); + deviceScreen.table.restore(); } private void showAppsSettings() { @@ -200,7 +170,7 @@ private void showAppsSettings() { if (resultList == null) return; PreferenceUtils.setPreference(PreferenceUtils.Pref.PREF_CUSTOM_APPS, GsonHelper.toJson(resultList)); - tableModel.setAppList(resultList); + deviceScreen.model.setAppList(resultList); } /** @@ -226,7 +196,7 @@ private List showMultilineEditDialog(String title, String message, List< JScrollPane scroll = new JScrollPane(inputField); panel.add(scroll, "grow, span, wrap"); - int rc = JOptionPane.showOptionDialog(frame, panel, title, JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE, null, null, null); + int rc = JOptionPane.showOptionDialog(deviceScreen, panel, title, JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE, null, null, null); if (rc != JOptionPane.YES_OPTION) return null; String results = inputField.getText(); @@ -249,7 +219,7 @@ private String showSingleLineEditDialog(String title, String message, String val JScrollPane scroll = new JScrollPane(inputField); panel.add(scroll, "grow, span, wrap"); - int rc = JOptionPane.showOptionDialog(frame, panel, title, JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE, null, null, null); + int rc = JOptionPane.showOptionDialog(deviceScreen, panel, title, JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE, null, null, null); if (rc != JOptionPane.YES_OPTION) return null; String results = inputField.getText().trim(); diff --git a/src/main/java/com/jpage4500/devicemanager/ui/views/CustomFrame.java b/src/main/java/com/jpage4500/devicemanager/ui/views/CustomFrame.java index 84aedec..045f1c4 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/views/CustomFrame.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/views/CustomFrame.java @@ -3,13 +3,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.swing.*; import java.awt.*; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.util.prefs.Preferences; -import javax.swing.*; - /** * */ @@ -49,7 +48,7 @@ public void componentMoved(ComponentEvent e) { }); } - private void saveFrameSize() { + protected void saveFrameSize() { Preferences prefs = Preferences.userRoot(); prefs.putInt(prefKey + "-" + FRAME_X, getX()); prefs.putInt(prefKey + "-" + FRAME_Y, getY()); @@ -69,5 +68,4 @@ private void restoreFrame() { setSize(w, h); } - } diff --git a/src/main/java/com/jpage4500/devicemanager/ui/views/CustomTable.java b/src/main/java/com/jpage4500/devicemanager/ui/views/CustomTable.java index 3f73b97..45e7a34 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/views/CustomTable.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/views/CustomTable.java @@ -1,6 +1,7 @@ package com.jpage4500.devicemanager.ui.views; import com.jpage4500.devicemanager.utils.GsonHelper; +import com.jpage4500.devicemanager.utils.PreferenceUtils; import com.jpage4500.devicemanager.utils.UiUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,6 +38,7 @@ public class CustomTable extends JTable { private int selectedColumn = -1; + private boolean showBackground; private String emptyText; private Image emptyImage; @@ -79,6 +81,8 @@ public CustomTable(String prefKey) { this.prefKey = prefKey; setOpaque(false); + showBackground = PreferenceUtils.getPreference(PreferenceUtils.PrefBoolean.PREF_SHOW_BACKGROUND, true); + createScrollPane(); setTableHeader(new CustomTableHeader(this)); @@ -128,12 +132,17 @@ public void setPopupMenuListener(PopupMenuListener popupMenuListener) { this.popupMenuListener = popupMenuListener; } + public void setEmptyText(String emptyText) { + this.emptyText = emptyText; + emptyImage = UiUtils.getImage("empty_image.png", 500); + } + private void createScrollPane() { scrollPane = new JScrollPane(this) { @Override public void paint(Graphics graphics) { super.paint(graphics); - if (emptyImage != null) { + if (emptyImage != null && showBackground) { int headerH = getTableHeader().getHeight(); int width = getWidth(); int imgW = emptyImage.getWidth(null); @@ -209,6 +218,11 @@ public void scrollRectToVisible(Rectangle aRect) { @Override public void setModel(TableModel dataModel) { super.setModel(dataModel); + + dataModel.addTableModelListener(tableModelEvent -> { + showBackground = PreferenceUtils.getPreference(PreferenceUtils.PrefBoolean.PREF_SHOW_BACKGROUND, true); + scrollPane.repaint(); + }); } @Override @@ -294,45 +308,59 @@ private void scrollPage(boolean isUp) { scrollRectToVisible(getCellRect(scrollToRow, 0, true)); } - public void setEmptyText(String emptyText) { - this.emptyText = emptyText; - - emptyImage = UiUtils.getImage("empty_image.png", 500); - } - public static class ColumnDetails { Object header; int width; + int maxWidth; int userPos; int modelPos; } - public void restore() { - if (prefKey == null) return; + public boolean restore() { + if (prefKey == null) return false; Preferences prefs = Preferences.userRoot(); String detailsStr = prefs.get(prefKey + "-details", null); - if (detailsStr == null) return; + if (detailsStr == null) return false; List detailsList = GsonHelper.stringToList(detailsStr, ColumnDetails.class); - //log.debug("restore: {}", GsonHelper.toJson(detailsList)); - - TableColumnModel columnModel = getColumnModel(); - if (detailsList.size() != columnModel.getColumnCount()) { - log.debug("restore: wrong number of columns! {} vs {}", detailsList.size(), columnModel.getColumnCount()); - return; - } - for (int i = 0; i < detailsList.size(); i++) { ColumnDetails details = detailsList.get(i); - //log.trace("restore: col:{}, w:{}", i, details.width); - columnModel.getColumn(i).setPreferredWidth(details.width); + // lookup column by name + TableColumn column = getColumnByName(details.header); + if (column == null) continue; + log.trace("restore: {}: w:{}, max:{}", details.header, details.width, details.maxWidth); + column.setPreferredWidth(details.width); + if (details.maxWidth > 0) column.setMaxWidth(details.maxWidth); + + int modelIndex = column.getModelIndex(); + if (modelIndex != details.userPos) { + log.trace("restore: moving: {}, from:{}, to:{}", details.header, modelIndex, details.userPos); + getColumnModel().moveColumn(modelIndex, details.userPos); + } } - for (ColumnDetails details : detailsList) { - if (details.modelPos != details.userPos) { - //log.trace("restore: move:{} to:{}", details.modelPos, details.userPos); - columnModel.moveColumn(details.modelPos, details.userPos); +// TableColumnModel columnModel = getColumnModel(); +// for (ColumnDetails details : detailsList) { +// if (details.modelPos != details.userPos) { +// log.trace("restore: move:{} to:{}", details.modelPos, details.userPos); +// columnModel.moveColumn(details.modelPos, details.userPos); +// } +// } + return true; + } + + /** + * get column by header name (NOTE: will return null and not throw an Exception when not found) + */ + public TableColumn getColumnByName(Object header) { + Enumeration columns = getColumnModel().getColumns(); + Iterator iterator = columns.asIterator(); + while (iterator.hasNext()) { + TableColumn column = iterator.next(); + if (column.getHeaderValue().equals(header)) { + return column; } } + return null; } public void persist() { @@ -348,7 +376,11 @@ public void persist() { details.userPos = i; details.modelPos = column.getModelIndex(); details.width = column.getWidth(); + int maxWidth = column.getMaxWidth(); + // only need to set maxWidth if one is defined (and it won't typicically be very large if it is) + if (maxWidth < 500) details.maxWidth = maxWidth; detailList.add(details); + log.trace("persist: {}", GsonHelper.toJson(details)); } Preferences prefs = Preferences.userRoot(); diff --git a/src/main/java/com/jpage4500/devicemanager/utils/FileUtils.java b/src/main/java/com/jpage4500/devicemanager/utils/FileUtils.java index 04a8748..3fc9ae5 100644 --- a/src/main/java/com/jpage4500/devicemanager/utils/FileUtils.java +++ b/src/main/java/com/jpage4500/devicemanager/utils/FileUtils.java @@ -70,7 +70,7 @@ public static String bytesToDisplayString(Long sizeInBytes) { return sizeDisplayFormat.format(sizeInBytes / Math.pow(1024, digitGroups)) + SIZE_UNITS[digitGroups]; } - private static final DecimalFormat sizeGigDisplayFormat = new DecimalFormat("#.0"); + private static final DecimalFormat sizeGigDisplayFormat = new DecimalFormat("0.0"); /** * return string description of number of bytes (in X.X gig) diff --git a/src/main/java/com/jpage4500/devicemanager/utils/PreferenceUtils.java b/src/main/java/com/jpage4500/devicemanager/utils/PreferenceUtils.java index 435dced..8d2098e 100644 --- a/src/main/java/com/jpage4500/devicemanager/utils/PreferenceUtils.java +++ b/src/main/java/com/jpage4500/devicemanager/utils/PreferenceUtils.java @@ -3,6 +3,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.prefs.BackingStoreException; import java.util.prefs.Preferences; public class PreferenceUtils { @@ -19,6 +20,7 @@ public enum Pref { PREF_RECENT_WIRELESS_DEVICES, PREF_LAST_DEVICE_IP, PREF_CUSTOM_COMMAND_LIST, + PREF_RECENT_INPUT, } /** @@ -29,6 +31,8 @@ public enum PrefBoolean { PREF_CHECK_UPDATES, PREF_ALWAYS_ON_TOP, PREF_USE_ROOT, + PREF_SHOW_BACKGROUND, + PREF_AUTO_FORMAT_MESSAGE, } /** @@ -62,30 +66,39 @@ public static void setPreference(PrefInt key, int value) { setPreference(key.name(), value); } + public static void resetAll() { + Preferences preferences = Preferences.userRoot(); + try { + preferences.clear(); + } catch (BackingStoreException e) { + log.error("resetAll: Exception: {}", e.getMessage()); + } + } + // ------------------------------------------------------------------------ // ------------------------------------------------------------------------ - public static String getPreference(String pref) { + private static String getPreference(String pref) { return getPreferences().get(pref, null); } - public static boolean getPreferenceBool(String pref, boolean defaultValue) { + private static boolean getPreferenceBool(String pref, boolean defaultValue) { return getPreferences().getBoolean(pref, defaultValue); } - public static int getPreferenceInt(String pref, int defaultValue) { + private static int getPreferenceInt(String pref, int defaultValue) { return getPreferences().getInt(pref, defaultValue); } - public static void setPreference(String key, String value) { + private static void setPreference(String key, String value) { getPreferences().put(key, value); } - public static void setPreference(String key, boolean value) { + private static void setPreference(String key, boolean value) { getPreferences().putBoolean(key, value); } - public static void setPreference(String key, int value) { + private static void setPreference(String key, int value) { getPreferences().putInt(key, value); } diff --git a/src/main/java/com/jpage4500/devicemanager/utils/TextUtils.java b/src/main/java/com/jpage4500/devicemanager/utils/TextUtils.java index ae34519..0928760 100644 --- a/src/main/java/com/jpage4500/devicemanager/utils/TextUtils.java +++ b/src/main/java/com/jpage4500/devicemanager/utils/TextUtils.java @@ -399,4 +399,95 @@ public static String join(List list, String separator) { } return sb.toString(); } + + /** + * very lenient JSON formatting + */ + public static String formatJson(String text) { + if (text == null) return null; + StringBuilder sb = new StringBuilder(); + int tabCount = 0; + boolean isInQuote = false; + char prevChar = 0; + + for (int i = 0; i < text.length(); i++) { + char ch = text.charAt(i); + + if (tabCount > 0 && ch == '"' && prevChar != '\\') { + isInQuote = !isInQuote; + } + + if (isInQuote || (tabCount <= 0 && ch != '{' && ch != '[')) { + sb.append(ch); + } else if (ch == '{') { + sb.append("\n"); + addTabs(sb, tabCount); + sb.append(ch); + sb.append("\n"); + tabCount++; + addTabs(sb, tabCount); + } else if (ch == '}') { + tabCount--; + trimEnd(sb); + sb.append("\n"); + + addTabs(sb, tabCount); + sb.append(ch); + if (tabCount > 0) { + sb.append("\n"); + addTabs(sb, tabCount); + } + } else if (ch == '[') { + sb.append("\n"); + addTabs(sb, tabCount); + sb.append(ch); + sb.append("\n"); + tabCount++; + addTabs(sb, tabCount); + } else if (ch == ']') { + tabCount--; + trimEnd(sb); + sb.append("\n"); + addTabs(sb, tabCount); + sb.append(ch); + sb.append("\n"); + addTabs(sb, tabCount); + } else if (ch == ',') { + trimEnd(sb); + sb.append("\n"); + addTabs(sb, tabCount); + } else if (ch == ':' && prevChar == '"') { + sb.append(" : "); + } else if ((ch == ' ' || ch == '\t')) { + // discard extra spaces + if (tabCount == 0) { + sb.append(ch); + } + } else if ((ch == '\n' || ch == '\r')) { + if (tabCount == 0) { + sb.append(ch); + } + // discard extra CR and LF + } else { + sb.append(ch); + } + + prevChar = ch; + } + return sb.toString(); + } + + private static void trimEnd(StringBuilder sb) { + // trim newline or space from end + char lastChar = sb.charAt(sb.length() - 1); + if (lastChar == '\n' || lastChar == ' ') { + sb.deleteCharAt(sb.length() - 1); + } + } + + private static void addTabs(StringBuilder sb, int tabCount) { + for (int i = 0; i < tabCount; i++) { + sb.append(" "); + } + } } diff --git a/src/main/java/se/vidstige/jadb/JadbDevice.java b/src/main/java/se/vidstige/jadb/JadbDevice.java index 3f4414c..2a5b4e1 100644 --- a/src/main/java/se/vidstige/jadb/JadbDevice.java +++ b/src/main/java/se/vidstige/jadb/JadbDevice.java @@ -281,36 +281,24 @@ public HashSet listRunningPackages() throws IOException, JadbException { } } - public void tap(int x, int y) throws IOException, JadbException { + public InputStream tap(int x, int y) throws IOException, JadbException { String cmd = String.format("input tap %s %s", x, y); - try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { - InputStream is = this.executeShell(cmd); - Stream.copy(is, baos); - } + return executeShell(cmd); } - public void swipe(int startX, int startY, int endX, int endY, int duration) throws IOException, JadbException { + public InputStream swipe(int startX, int startY, int endX, int endY, int duration) throws IOException, JadbException { String cmd = String.format("input swipe %s %s %s %s %s", startX, startY, endX, endY, duration); - try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { - InputStream is = this.executeShell(cmd); - Stream.copy(is, baos); - } + return executeShell(cmd); } - public void inputText(String text) throws IOException, JadbException { + public InputStream inputText(String text) throws IOException, JadbException { String cmd = String.format("input text %s", text); - try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { - InputStream is = this.executeShell(cmd); - Stream.copy(is, baos); - } + return executeShell(cmd); } - public void inputKeyEvent(int keyEvent) throws IOException, JadbException { + public InputStream inputKeyEvent(int keyEvent) throws IOException, JadbException { String cmd = String.format("input keyevent %s", keyEvent); - try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { - InputStream is = this.executeShell(cmd); - Stream.copy(is, baos); - } + return executeShell(cmd); } public Boolean isInstalled(String pkgName) throws IOException, JadbException { diff --git a/src/main/resources/images/json.png b/src/main/resources/images/json.png new file mode 100644 index 0000000000000000000000000000000000000000..4679c89dff1eb824484bbc881dcb925b71cea220 GIT binary patch literal 9266 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 NY%Cot9-Fye`58d_F9-kt literal 0 HcmV?d00001 diff --git a/src/main/resources/images/wrap.png b/src/main/resources/images/wrap.png new file mode 100644 index 0000000000000000000000000000000000000000..befd23b5e196c4f4272ce230ffe9cb268b63ce1c GIT binary patch literal 7426 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 literal 0 HcmV?d00001 From 6f1366aa0426120e2a53d6799cdb113eabf9cb80 Mon Sep 17 00:00:00 2001 From: Joe Page Date: Thu, 13 Jun 2024 18:03:40 -0400 Subject: [PATCH 03/11] - added log message viewer - WIP on log filter editor --- .../devicemanager/data/LogFilter.java | 77 ++++--- .../devicemanager/table/LogsTableModel.java | 4 +- .../devicemanager/ui/DeviceScreen.java | 19 +- .../devicemanager/ui/ExploreScreen.java | 2 +- .../devicemanager/ui/LogsScreen.java | 193 ++++++++++++++---- .../devicemanager/ui/MessageViewScreen.java | 88 ++++++-- .../ui/dialog/AddFilterDialog.java | 49 +++++ .../devicemanager/ui/views/CustomTable.java | 22 +- .../devicemanager/utils/PreferenceUtils.java | 17 ++ .../devicemanager/utils/TextUtils.java | 32 +-- 10 files changed, 384 insertions(+), 119 deletions(-) create mode 100644 src/main/java/com/jpage4500/devicemanager/ui/dialog/AddFilterDialog.java diff --git a/src/main/java/com/jpage4500/devicemanager/data/LogFilter.java b/src/main/java/com/jpage4500/devicemanager/data/LogFilter.java index 3674c9d..bfa1801 100644 --- a/src/main/java/com/jpage4500/devicemanager/data/LogFilter.java +++ b/src/main/java/com/jpage4500/devicemanager/data/LogFilter.java @@ -28,25 +28,30 @@ public String toString() { if (column == null) sb.append("*"); else sb.append(column.name().toLowerCase()); sb.append(":"); - if (isNotExpression) sb.append("!"); - switch (expression) { - case STARTS_WITH: - sb.append("*"); - sb.append(value); - break; - case ENDS_WITH: - sb.append(value); - sb.append("*"); - break; - case CONTAINS: - sb.append("*"); - sb.append(value); - sb.append("*"); - break; - default: - sb.append(value); - break; + if (TextUtils.equalsIgnoreCaseAny(value, "*", "")) { + sb.append("*"); + } else { + if (isNotExpression) sb.append("!"); + switch (expression) { + case STARTS_WITH: + sb.append(value); + sb.append("*"); + break; + case ENDS_WITH: + sb.append("*"); + sb.append(value); + break; + case CONTAINS: + sb.append("*"); + sb.append(value); + sb.append("*"); + break; + default: + sb.append(value); + break; + } } + return sb.toString(); } @@ -121,45 +126,49 @@ public String toString() { public static LogFilter parse(String filterText) { if (filterText == null) return null; - LogFilter filter = null; + LogFilter filter = new LogFilter(); + filter.filterList = new ArrayList<>(); // TODO: support more than just "&&" (ie: "||") String[] filterArr = filterText.split(" && "); for (String entry : filterArr) { String[] entryArr = entry.split(":", 2); + String key = entryArr[0].trim(); + String value = entryArr[1].trim(); LogFilter.FilterExpression expr = new LogFilter.FilterExpression(); - String colName = entryArr[0].toUpperCase(); - try { - expr.column = LogsTableModel.Columns.valueOf(colName); - } catch (IllegalArgumentException e) { + if (TextUtils.notEmpty(key)) { + String colName = key.toUpperCase(); + try { + expr.column = LogsTableModel.Columns.valueOf(colName); + } catch (IllegalArgumentException e) { + } + } + if (TextUtils.equalsIgnoreCaseAny(value, "*", "")) { + filter.filterList.add(expr); + continue; } - String value = entryArr[1].trim(); - if (TextUtils.isEmpty(value)) continue; char firstChar = value.charAt(0); if (firstChar == '!') { expr.isNotExpression = true; firstChar = value.charAt(1); } if (firstChar == '*') { - expr.expression = Expression.STARTS_WITH; + expr.expression = Expression.ENDS_WITH; } char lastChar = value.charAt(value.length() - 1); if (lastChar == '*' || (expr.column == LogsTableModel.Columns.LEVEL && lastChar == '+')) { - if (expr.expression == Expression.STARTS_WITH) expr.expression = Expression.CONTAINS; - else expr.expression = Expression.ENDS_WITH; + if (expr.expression == Expression.ENDS_WITH) expr.expression = Expression.CONTAINS; + else expr.expression = Expression.STARTS_WITH; } int stPos = 0; if (expr.isNotExpression) stPos++; - if (expr.expression == Expression.STARTS_WITH || expr.expression == Expression.CONTAINS) stPos++; + if (expr.expression == Expression.ENDS_WITH || expr.expression == Expression.CONTAINS) stPos++; int endPos = value.length(); - if (expr.expression == Expression.ENDS_WITH || expr.expression == Expression.CONTAINS) endPos--; + if (expr.expression == Expression.STARTS_WITH || expr.expression == Expression.CONTAINS) endPos--; expr.value = value.substring(stPos, endPos); - if (filter == null) { - filter = new LogFilter(); - filter.filterList = new ArrayList<>(); - } + //log.trace("parse: expr:{}", GsonHelper.toJson(expr)); filter.filterList.add(expr); } return filter; diff --git a/src/main/java/com/jpage4500/devicemanager/table/LogsTableModel.java b/src/main/java/com/jpage4500/devicemanager/table/LogsTableModel.java index f5727f5..ce42bc0 100644 --- a/src/main/java/com/jpage4500/devicemanager/table/LogsTableModel.java +++ b/src/main/java/com/jpage4500/devicemanager/table/LogsTableModel.java @@ -75,7 +75,9 @@ public void addLogEntry(List logEntryList) { public void setProcessMap(Map processMap) { this.processMap.clear(); this.processMap.putAll(processMap); - fireTableDataChanged(); + + // NOTE: is it worth refreshing all rows just to update old log entries? + //fireTableDataChanged(); } public void setSearchText(String text) { diff --git a/src/main/java/com/jpage4500/devicemanager/ui/DeviceScreen.java b/src/main/java/com/jpage4500/devicemanager/ui/DeviceScreen.java index f741275..aec95ff 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/DeviceScreen.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/DeviceScreen.java @@ -242,7 +242,7 @@ private void setupTable() { sorter = new DeviceRowSorter(model); table.setRowSorter(sorter); - table.setClickListener((row, column, e) -> { + table.setDoubleClickListener((row, column, e) -> { if (column == DeviceTableModel.Columns.CUSTOM1.ordinal()) { // edit custom 1 field handleSetProperty(1); @@ -426,11 +426,6 @@ private void refreshUi() { } private void updateVersionLabel() { - long totalMemory = Runtime.getRuntime().totalMemory(); - long freeMemory = Runtime.getRuntime().freeMemory(); - long usedMemory = totalMemory - freeMemory; - String memUsage = FileUtils.bytesToDisplayString(usedMemory); - String versionText; if (newerRelease != null) { versionText = "Update Available: " + newerRelease.tagName; @@ -438,7 +433,15 @@ private void updateVersionLabel() { versionText = "v" + MainApplication.version; } - statusBar.setLeftLabel(versionText + " / " + memUsage); + boolean debugMode = PreferenceUtils.getPreference(PreferenceUtils.PrefBoolean.PREF_DEBUG_MODE, false); + if (debugMode) { + long totalMemory = Runtime.getRuntime().totalMemory(); + long freeMemory = Runtime.getRuntime().freeMemory(); + long usedMemory = totalMemory - freeMemory; + versionText += " / " + FileUtils.bytesToDisplayString(usedMemory); + } + + statusBar.setLeftLabel(versionText); } private void handleHideColumn(int column) { @@ -774,7 +777,7 @@ private void setupToolbar() { createToolbarButton(toolbar, "icon_browse.png", "Browse", "File Explorer", actionEvent -> handleBrowseCommand()); - createToolbarButton(toolbar, "logs.png", "Logs", "Log Viewer", actionEvent -> handleLogsCommand()); + createToolbarButton(toolbar, "icon_script.png", "Logs", "Log Viewer", actionEvent -> handleLogsCommand()); createToolbarButton(toolbar, "keyboard.png", "Input", "Enter text", actionEvent -> handleInputCommand()); diff --git a/src/main/java/com/jpage4500/devicemanager/ui/ExploreScreen.java b/src/main/java/com/jpage4500/devicemanager/ui/ExploreScreen.java index 34b97f9..61d89b2 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/ExploreScreen.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/ExploreScreen.java @@ -211,7 +211,7 @@ public void actionPerformed(ActionEvent e) { } }); - table.setClickListener((row, column, e) -> handleFileClicked()); + table.setDoubleClickListener((row, column, e) -> handleFileClicked()); table.setPopupMenuListener((row, column) -> { // TODO diff --git a/src/main/java/com/jpage4500/devicemanager/ui/LogsScreen.java b/src/main/java/com/jpage4500/devicemanager/ui/LogsScreen.java index 6fdf27f..be808d1 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/LogsScreen.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/LogsScreen.java @@ -9,10 +9,12 @@ import com.jpage4500.devicemanager.table.utils.LogsCellRenderer; import com.jpage4500.devicemanager.table.utils.LogsRowSorter; import com.jpage4500.devicemanager.table.utils.TableColumnAdjuster; +import com.jpage4500.devicemanager.ui.dialog.AddFilterDialog; import com.jpage4500.devicemanager.ui.views.CustomTable; import com.jpage4500.devicemanager.ui.views.HintTextField; import com.jpage4500.devicemanager.ui.views.StatusBar; import com.jpage4500.devicemanager.utils.GsonHelper; +import com.jpage4500.devicemanager.utils.PreferenceUtils; import com.jpage4500.devicemanager.utils.TextUtils; import com.jpage4500.devicemanager.utils.UiUtils; import org.slf4j.Logger; @@ -22,14 +24,13 @@ import javax.swing.border.EmptyBorder; import javax.swing.table.TableColumnModel; import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.InputEvent; -import java.awt.event.KeyEvent; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.StringSelection; +import java.awt.event.*; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; -import java.util.prefs.Preferences; /** * create and manage device view @@ -50,8 +51,11 @@ public class LogsScreen extends BaseScreen implements DeviceManager.DeviceLogLis public JToolBar toolbar; private JCheckBox autoScrollCheckBox; private HintTextField searchField; + + // filter logs private HintTextField filterField; private JList filterList; + private int numSystemFilters; private LogsRowSorter sorter; private MessageViewScreen viewScreen; @@ -91,15 +95,24 @@ protected void initalizeUi() { setupToolbar(); mainPanel.add(toolbar, BorderLayout.NORTH); - // -- left panel -- + // ** left panel ** JPanel leftPanel = new JPanel(new BorderLayout()); + + // -- filter text -- filterField = new HintTextField(HINT_FILTER, this::filterDevices); leftPanel.add(filterField, BorderLayout.NORTH); + // -- filter list -- filterList = new JList<>(); setupFilterList(); leftPanel.add(filterList, BorderLayout.CENTER); + // -- add filter button -- + JButton addFilterButton = new JButton("Add Filter"); + addFilterButton.setIcon(UiUtils.getImageIcon("icon_add.png", 20)); + addFilterButton.addActionListener(this::handleAddFilterClicked); + leftPanel.add(addFilterButton, BorderLayout.SOUTH); + JPanel rightPanel = new JPanel(new BorderLayout()); // -- table -- @@ -123,6 +136,15 @@ protected void initalizeUi() { setVisible(true); table.requestFocus(); autoScrollCheckBox.setSelected(true); + + // restore previous filter + String recentFilterText = PreferenceUtils.getPreference(PreferenceUtils.Pref.PREF_RECENT_MESSAGE_FILTER); + filterField.setText(recentFilterText); + } + + private void handleAddFilterClicked(ActionEvent actionEvent) { + // TODO... + AddFilterDialog.showAddFilterDialog(this, null); } @Override @@ -198,7 +220,7 @@ private void setupMenuBar() { createCmdAction(logsMenu, "Clear logs", KeyEvent.VK_K, e -> model.clearLogs()); // [CMD + F] = focus search field - createCmdAction(windowMenu, "Search for...", KeyEvent.VK_1, e -> searchField.requestFocus()); + createCmdAction(logsMenu, "Search for...", KeyEvent.VK_F, e -> searchField.requestFocus()); JMenuBar menubar = new JMenuBar(); menubar.add(windowMenu); @@ -208,6 +230,9 @@ private void setupMenuBar() { private void closeWindow() { log.trace("closeWindow: {}", device.getDisplayName()); + // save last filter + String filterText = filterField.getCleanText(); + PreferenceUtils.setPreference(PreferenceUtils.Pref.PREF_RECENT_MESSAGE_FILTER, filterText); //stopLogging(); deviceScreen.handleLogsClosed(device.serial); dispose(); @@ -228,8 +253,11 @@ private void setupTable() { // default column sizes TableColumnModel columnModel = table.getColumnModel(); columnModel.getColumn(LogsTableModel.Columns.LEVEL.ordinal()).setPreferredWidth(28); + columnModel.getColumn(LogsTableModel.Columns.LEVEL.ordinal()).setMaxWidth(35); columnModel.getColumn(LogsTableModel.Columns.PID.ordinal()).setPreferredWidth(60); + columnModel.getColumn(LogsTableModel.Columns.PID.ordinal()).setMaxWidth(100); columnModel.getColumn(LogsTableModel.Columns.TID.ordinal()).setPreferredWidth(60); + columnModel.getColumn(LogsTableModel.Columns.TID.ordinal()).setMaxWidth(100); columnModel.getColumn(LogsTableModel.Columns.DATE.ordinal()).setPreferredWidth(159); columnModel.getColumn(LogsTableModel.Columns.APP.ordinal()).setPreferredWidth(150); columnModel.getColumn(LogsTableModel.Columns.MSG.ordinal()).setPreferredWidth(700); @@ -261,13 +289,13 @@ public void actionPerformed(ActionEvent e) { // if row selected, stop auto-scroll int numSelected = table.getSelectedRowCount(); if (numSelected > 0 && autoScrollCheckBox.isSelected()) { - log.debug("setupTable: disabled auto scroll"); + log.trace("setupTable: disabled auto scroll"); autoScrollCheckBox.setSelected(false); } }); table.setPopupMenuListener((row, column) -> { - JPopupMenu popupMenu = null; + JPopupMenu popupMenu = new JPopupMenu(); if (row == -1) { JMenuItem sizeToFitItem = new JMenuItem("Size to Fit"); sizeToFitItem.addActionListener(actionEvent -> { @@ -275,30 +303,40 @@ public void actionPerformed(ActionEvent e) { int tableCol = table.convertColumnIndexToView(column); adjuster.adjustColumn(tableCol); }); - popupMenu = new JPopupMenu(); popupMenu.add(sizeToFitItem); return popupMenu; } - LogsTableModel.Columns columnType = model.getColumnType(column); - switch (columnType) { - case APP: - case TID: - case PID: - case LEVEL: - case TAG: - // filter by value - String text = model.getTextValue(row, column); - JMenuItem copyFieldItem = new JMenuItem("Add Filter"); - copyFieldItem.addActionListener(actionEvent -> handleAddFilter(columnType, text)); - popupMenu = new JPopupMenu(); - popupMenu.add(copyFieldItem); + int selectedRows = table.getSelectedRowCount(); + if (selectedRows == 1) { + LogsTableModel.Columns columnType = model.getColumnType(column); + switch (columnType) { + case APP: + case TID: + case PID: + case LEVEL: + case TAG: + // filter by value + String text = model.getTextValue(row, column); + JMenuItem copyFieldItem = new JMenuItem("Add Filter"); + copyFieldItem.addActionListener(actionEvent -> handleQuickAddFilter(columnType, text)); + popupMenu.add(copyFieldItem); + } } + // copy line(s) + JMenuItem copyItem = new JMenuItem("Copy"); + copyItem.addActionListener(actionEvent -> handleCopyClicked()); + popupMenu.add(copyItem); + + // copy message + JMenuItem copyMessageItem = new JMenuItem("Copy Message"); + copyMessageItem.addActionListener(actionEvent -> handleCopyMessageClicked()); + popupMenu.add(copyMessageItem); + // view message JMenuItem viewItem = new JMenuItem("View Message"); viewItem.addActionListener(actionEvent -> handleLogClicked()); - if (popupMenu == null) popupMenu = new JPopupMenu(); popupMenu.add(viewItem); return popupMenu; @@ -307,7 +345,7 @@ public void actionPerformed(ActionEvent e) { sorter = new LogsRowSorter(model); table.setRowSorter(sorter); - table.setClickListener((row, column, e) -> { + table.setDoubleClickListener((row, column, e) -> { LogEntry logEntry = (LogEntry) model.getValueAt(row, column); if (logEntry == null) return; viewMessage(logEntry); @@ -337,7 +375,54 @@ public void actionPerformed(ActionEvent e) { }); } + private void handleCopyMessageClicked() { + List logEntryList = getSelectedLogEntries(); + StringBuilder sb = new StringBuilder(); + for (LogEntry logEntry : logEntryList) { + if (!sb.isEmpty()) sb.append("\n"); + sb.append(logEntry.message); + } + if (sb.isEmpty()) return; + + StringSelection stringSelection = new StringSelection(sb.toString()); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + clipboard.setContents(stringSelection, null); + } + + private void handleCopyClicked() { + List logEntryList = getSelectedLogEntries(); + StringBuilder sb = new StringBuilder(); + for (LogEntry logEntry : logEntryList) { + if (!sb.isEmpty()) sb.append("\n"); + sb.append(logEntry.date); + sb.append(", "); + sb.append(logEntry.app); + sb.append(", "); + sb.append(logEntry.tid); + sb.append(", "); + sb.append(logEntry.pid); + sb.append(", "); + sb.append(logEntry.level); + sb.append(", "); + sb.append(logEntry.tag); + sb.append(", "); + sb.append(logEntry.message); + } + if (sb.isEmpty()) return; + + StringSelection stringSelection = new StringSelection(sb.toString()); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + clipboard.setContents(stringSelection, null); + } + private void handleLogClicked() { + List logEntryList = getSelectedLogEntries(); + if (!logEntryList.isEmpty()) { + viewMessage(logEntryList.toArray(new LogEntry[0])); + } + } + + private List getSelectedLogEntries() { List logEntryList = new ArrayList<>(); int[] selectedRows = table.getSelectedRows(); for (int selectedRow : selectedRows) { @@ -345,18 +430,16 @@ private void handleLogClicked() { LogEntry logEntry = (LogEntry) model.getValueAt(realRow, 0); logEntryList.add(logEntry); } - if (!logEntryList.isEmpty()) { - viewMessage(logEntryList.toArray(new LogEntry[0])); - } + return logEntryList; } private void viewMessage(LogEntry... logEntry) { - if (viewScreen == null) viewScreen = new MessageViewScreen(); + if (viewScreen == null) viewScreen = new MessageViewScreen(deviceScreen); viewScreen.setLogEntry(logEntry); viewScreen.setVisible(true); } - private void handleAddFilter(LogsTableModel.Columns columnType, String text) { + private void handleQuickAddFilter(LogsTableModel.Columns columnType, String text) { LogFilter filter = LogFilter.parse(columnType.name().toLowerCase() + ":" + text); filterField.setText(filter.toString()); } @@ -450,23 +533,61 @@ private void doSearch(String text) { } private void setupFilterList() { - Preferences preferences = Preferences.userRoot(); - String prefListStr = preferences.get("PREF_FILTER_LIST", null); - List filterItemList = GsonHelper.stringToList(prefListStr, FilterItem.class); + populateFilters(); + filterList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + filterList.addListSelectionListener(e -> filterDevices(filterField.getCleanText())); + filterList.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + // single click + Point point = e.getPoint(); + int i = filterList.locationToIndex(point); + if (i < 0) return; + if (SwingUtilities.isRightMouseButton(e)) { + // select item + filterList.setSelectedIndex(i); + + // only show popup menu for user filters + if (i < numSystemFilters) return; + + JPopupMenu popupMenu = new JPopupMenu(); + JMenuItem editItem = new JMenuItem("Edit Filter"); + editItem.addActionListener(actionEvent -> handleEditFilterClicked()); + popupMenu.add(editItem); + JMenuItem deleteItem = new JMenuItem("Delete Filter"); + deleteItem.addActionListener(actionEvent -> handleDeleteFilterClicked()); + popupMenu.add(deleteItem); + } + } + }); + } - // add basic log level filters + private void populateFilters() { + List filterItemList = new ArrayList<>(); + // -- system filteres -- addLogLevel(filterItemList, "All Messages", null); addLogLevel(filterItemList, "Log Level Debug+", "level:D+"); addLogLevel(filterItemList, "Log Level Info+", "level:I+"); addLogLevel(filterItemList, "Log Level Warn+", "level:W+"); addLogLevel(filterItemList, "Log Level Error+", "level:E"); + numSystemFilters = filterItemList.size(); + // -- user filters -- + String filterStr = PreferenceUtils.getPreference(PreferenceUtils.Pref.PREF_MESSAGE_FILTERS); + List userFilterList = GsonHelper.stringToList(filterStr, FilterItem.class); // sort A-Z (name) - filterItemList.sort((lhs, rhs) -> TextUtils.compareToIgnoreCase(lhs.name, rhs.name)); + userFilterList.sort((lhs, rhs) -> TextUtils.compareToIgnoreCase(lhs.name, rhs.name)); + filterItemList.addAll(userFilterList); filterList.setListData(filterItemList.toArray(new FilterItem[0])); - filterList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - filterList.addListSelectionListener(e -> filterDevices(filterField.getCleanText())); + } + + private void handleDeleteFilterClicked() { + + } + + private void handleEditFilterClicked() { + } private void addLogLevel(List filterItemList, String label, String filter) { diff --git a/src/main/java/com/jpage4500/devicemanager/ui/MessageViewScreen.java b/src/main/java/com/jpage4500/devicemanager/ui/MessageViewScreen.java index 9e8dc25..b143246 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/MessageViewScreen.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/MessageViewScreen.java @@ -3,12 +3,17 @@ import com.jpage4500.devicemanager.data.LogEntry; import com.jpage4500.devicemanager.utils.PreferenceUtils; import com.jpage4500.devicemanager.utils.TextUtils; +import com.jpage4500.devicemanager.utils.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.swing.*; import java.awt.*; import java.awt.event.KeyEvent; +import java.io.File; +import java.io.FileOutputStream; +import java.io.PrintStream; +import java.util.Random; /** * view log messages @@ -21,19 +26,26 @@ public class MessageViewScreen extends BaseScreen { public static final String TEXT_WRAP_ON = "Wrap ON"; public static final String TEXT_AUTO_FORMAT_ON = "Auto Format ON"; public static final String TEXT_AUTO_FORMAT_OFF = "Auto Format OFF"; + public static final String TEXT_WRAP_OFF = "Wrap OFF"; + + private final DeviceScreen deviceScreen; private LogEntry[] logEntryArr; private JTextArea textArea; + private JScrollPane scrollPane; private JButton jsonButton; private JButton xmlButton; private JButton wrapButton; private JButton autoFormatButton; + private JButton editButton; - public MessageViewScreen() { + public MessageViewScreen(DeviceScreen deviceScreen) { super("message"); - setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + this.deviceScreen = deviceScreen; + setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE); initalizeUi(); + refreshUi(); } protected void initalizeUi() { @@ -48,7 +60,7 @@ protected void initalizeUi() { textArea = new JTextArea(); textArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); - JScrollPane scrollPane = new JScrollPane(textArea); + scrollPane = new JScrollPane(textArea); mainPanel.add(scrollPane, BorderLayout.CENTER); setupMenuBar(); @@ -62,10 +74,28 @@ protected void initalizeUi() { private void setupToolbar(JToolBar toolbar) { //toolbar.add(Box.createHorizontalGlue()); - jsonButton = createToolbarButton(toolbar, "json.png", TEXT_FORMAT_JSON, "Format JSON", actionEvent -> toggleJson()); - xmlButton = createToolbarButton(toolbar, "xml.png", TEXT_FORMAT_XML, "Format XML", actionEvent -> formatXml()); - wrapButton = createToolbarButton(toolbar, "wrap.png", TEXT_WRAP_ON, "Wrap ON", actionEvent -> toggleWrap()); - autoFormatButton = createToolbarButton(toolbar, null, TEXT_AUTO_FORMAT_ON, "Auto Format ON", actionEvent -> toggleAutoFormat()); + jsonButton = createSmallToolbarButton(toolbar, "json.png", TEXT_FORMAT_JSON, "Format JSON text (pretty-print)", actionEvent -> toggleJson()); + //xmlButton = createSmallToolbarButton(toolbar, "xml.png", TEXT_FORMAT_XML, "Format XML", actionEvent -> formatXml()); + wrapButton = createSmallToolbarButton(toolbar, "wrap.png", TEXT_WRAP_ON, "Wrap text (line wrap)", actionEvent -> toggleWrap()); + autoFormatButton = createSmallToolbarButton(toolbar, "status_busy.png", TEXT_AUTO_FORMAT_ON, "Auto Format JSON text", actionEvent -> toggleAutoFormat()); + editButton = createSmallToolbarButton(toolbar, "icon_edit.png", "Edit", "Edit message in default editor", actionEvent -> editMessage()); + } + + private void editMessage() { + // save to temp file + String tempFolder = System.getProperty("java.io.tmpdir"); + Random rand = new Random(); + int randInt = rand.nextInt(1000); + File tempFile = new File(tempFolder, "msg-" + randInt + ".txt"); + try (PrintStream out = new PrintStream(new FileOutputStream(tempFile))) { + out.print(textArea.getText()); + out.flush(); + } catch (Exception e) { + log.error("editMessage: Exception: {}", e.getMessage()); + return; + } + + Utils.editFile(tempFile); } private void setupMenuBar() { @@ -74,25 +104,44 @@ private void setupMenuBar() { // [CMD + W] = close window createCmdAction(windowMenu, "Close Window", KeyEvent.VK_W, e -> closeWindow()); + // [CMD + 1] = show devices + createCmdAction(windowMenu, "Show Devices", KeyEvent.VK_1, e -> deviceScreen.toFront()); + + // [CMD + 2] = show explorer + createCmdAction(windowMenu, "Browse Files", KeyEvent.VK_2, e -> deviceScreen.handleBrowseCommand()); + + JMenu messageMenu = new JMenu("Message"); + + // [CMD + E] = edit message + createCmdAction(messageMenu, "Edit Message", KeyEvent.VK_E, e -> editMessage()); + JMenuBar menubar = new JMenuBar(); menubar.add(windowMenu); setJMenuBar(menubar); } private void closeWindow() { - log.trace("closeWindow:"); - dispose(); + setVisible(false); + //dispose(); } public void setLogEntry(LogEntry... logEntryArr) { this.logEntryArr = logEntryArr; + refreshUi(); + boolean autoFormat = PreferenceUtils.getPreference(PreferenceUtils.PrefBoolean.PREF_AUTO_FORMAT_MESSAGE, true); - if (autoFormat) { + if (autoFormat && TextUtils.containsJson(getLogText())) { formatJson(); } else { restoreText(); } + + // scroll back to top/left + SwingUtilities.invokeLater(() -> { + scrollPane.getVerticalScrollBar().setValue(0); + scrollPane.getHorizontalScrollBar().setValue(0); + }); } private String getLogText() { @@ -105,8 +154,8 @@ private String getLogText() { } private void toggleAutoFormat() { - boolean autoFormat = !PreferenceUtils.getPreference(PreferenceUtils.PrefBoolean.PREF_AUTO_FORMAT_MESSAGE, true); - autoFormatButton.setText(autoFormat ? TEXT_AUTO_FORMAT_ON : TEXT_AUTO_FORMAT_OFF); + PreferenceUtils.togglePreference(PreferenceUtils.PrefBoolean.PREF_AUTO_FORMAT_MESSAGE, true); + refreshUi(); } private void formatXml() { @@ -114,9 +163,18 @@ private void formatXml() { } private void toggleWrap() { - boolean lineWrap = !textArea.getLineWrap(); - textArea.setLineWrap(lineWrap); - textArea.setWrapStyleWord(lineWrap); + PreferenceUtils.togglePreference(PreferenceUtils.PrefBoolean.PREF_WRAP_MESSAGE, false); + refreshUi(); + } + + private void refreshUi() { + boolean autoFormat = PreferenceUtils.getPreference(PreferenceUtils.PrefBoolean.PREF_AUTO_FORMAT_MESSAGE, true); + autoFormatButton.setText(autoFormat ? TEXT_AUTO_FORMAT_ON : TEXT_AUTO_FORMAT_OFF); + + boolean wrapMessage = PreferenceUtils.getPreference(PreferenceUtils.PrefBoolean.PREF_WRAP_MESSAGE, false); + wrapButton.setText(wrapMessage ? TEXT_WRAP_ON : TEXT_WRAP_OFF); + textArea.setLineWrap(wrapMessage); + textArea.setWrapStyleWord(wrapMessage); } private void toggleJson() { diff --git a/src/main/java/com/jpage4500/devicemanager/ui/dialog/AddFilterDialog.java b/src/main/java/com/jpage4500/devicemanager/ui/dialog/AddFilterDialog.java new file mode 100644 index 0000000..f443e99 --- /dev/null +++ b/src/main/java/com/jpage4500/devicemanager/ui/dialog/AddFilterDialog.java @@ -0,0 +1,49 @@ +package com.jpage4500.devicemanager.ui.dialog; + +import com.jpage4500.devicemanager.data.LogFilter; +import net.miginfocom.swing.MigLayout; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.swing.*; +import java.awt.*; + +public class AddFilterDialog extends JPanel { + private static final Logger log = LoggerFactory.getLogger(AddFilterDialog.class); + + private LogFilter logFilter; + + public static LogFilter showAddFilterDialog(Component frame, LogFilter logFilter) { + AddFilterDialog screen = new AddFilterDialog(logFilter); + int rc = JOptionPane.showOptionDialog(frame, screen, "Add Filter", JOptionPane.DEFAULT_OPTION, + JOptionPane.PLAIN_MESSAGE, null, new Object[]{}, null); + if (rc != JOptionPane.YES_OPTION) return null; + + return screen.logFilter; + } + + public AddFilterDialog(LogFilter logFilter) { + logFilter = logFilter; + if (logFilter == null) logFilter = new LogFilter(); + + initalizeUi(); + } + + protected void initalizeUi() { + setLayout(new MigLayout("fillx", "[][]")); + + add(new JLabel("Filter Name")); + JTextField nameField = new JTextField(); + add(nameField); + + JButton addButton = new JButton("Add Filter"); + addButton.addActionListener(e -> handleAddClicked()); + add(addButton, "al right, span 2, wrap"); + } + + private void handleAddClicked() { + + } + +} + diff --git a/src/main/java/com/jpage4500/devicemanager/ui/views/CustomTable.java b/src/main/java/com/jpage4500/devicemanager/ui/views/CustomTable.java index 45e7a34..33942f8 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/views/CustomTable.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/views/CustomTable.java @@ -7,7 +7,10 @@ import org.slf4j.LoggerFactory; import javax.swing.*; -import javax.swing.table.*; +import javax.swing.table.JTableHeader; +import javax.swing.table.TableCellRenderer; +import javax.swing.table.TableColumn; +import javax.swing.table.TableModel; import java.awt.*; import java.awt.dnd.DropTarget; import java.awt.dnd.DropTargetDragEvent; @@ -30,9 +33,8 @@ public class CustomTable extends JTable { private static final Color COLOR_ALTERNATE_ROW = new Color(246, 246, 246); private String prefKey; - private ClickListener listener; private TooltipListener tooltipListener; - private ClickListener clickListener; + private DoubleClickListener doubleClickListener; private PopupMenuListener popupMenuListener; private JScrollPane scrollPane; @@ -42,7 +44,7 @@ public class CustomTable extends JTable { private String emptyText; private Image emptyImage; - public interface ClickListener { + public interface DoubleClickListener { /** * @param row converted to model row * @param column converted to model col @@ -114,14 +116,14 @@ public void mouseClicked(MouseEvent e) { // convert table row/col to model row/col row = convertRowIndexToModel(row); column = convertColumnIndexToModel(column); - if (listener != null) listener.handleDoubleClick(row, column, e); + if (doubleClickListener != null) doubleClickListener.handleDoubleClick(row, column, e); } } }); } - public void setClickListener(ClickListener listener) { - this.listener = listener; + public void setDoubleClickListener(DoubleClickListener doubleClickListener) { + this.doubleClickListener = doubleClickListener; } public void setTooltipListener(TooltipListener tooltipListener) { @@ -327,7 +329,7 @@ public boolean restore() { // lookup column by name TableColumn column = getColumnByName(details.header); if (column == null) continue; - log.trace("restore: {}: w:{}, max:{}", details.header, details.width, details.maxWidth); + //log.trace("restore: {}: w:{}, max:{}", details.header, details.width, details.maxWidth); column.setPreferredWidth(details.width); if (details.maxWidth > 0) column.setMaxWidth(details.maxWidth); @@ -380,7 +382,7 @@ public void persist() { // only need to set maxWidth if one is defined (and it won't typicically be very large if it is) if (maxWidth < 500) details.maxWidth = maxWidth; detailList.add(details); - log.trace("persist: {}", GsonHelper.toJson(details)); + //log.trace("persist: {}", GsonHelper.toJson(details)); } Preferences prefs = Preferences.userRoot(); @@ -411,10 +413,10 @@ public void mouseClicked(MouseEvent e) { if (SwingUtilities.isRightMouseButton(e)) { if (popupMenuListener != null) { Point point = e.getPoint(); - int row = rowAtPoint(point); int column = columnAtPoint(point); // convert table row/col to model row/col column = convertColumnIndexToModel(column); + // NOTE: row fixed at -1 for header JPopupMenu popupMenu = popupMenuListener.getPopupMenu(-1, column); if (popupMenu != null) popupMenu.show(e.getComponent(), e.getX(), e.getY()); } diff --git a/src/main/java/com/jpage4500/devicemanager/utils/PreferenceUtils.java b/src/main/java/com/jpage4500/devicemanager/utils/PreferenceUtils.java index 8d2098e..44310aa 100644 --- a/src/main/java/com/jpage4500/devicemanager/utils/PreferenceUtils.java +++ b/src/main/java/com/jpage4500/devicemanager/utils/PreferenceUtils.java @@ -21,6 +21,8 @@ public enum Pref { PREF_LAST_DEVICE_IP, PREF_CUSTOM_COMMAND_LIST, PREF_RECENT_INPUT, + PREF_RECENT_MESSAGE_FILTER, + PREF_MESSAGE_FILTERS, } /** @@ -33,6 +35,7 @@ public enum PrefBoolean { PREF_USE_ROOT, PREF_SHOW_BACKGROUND, PREF_AUTO_FORMAT_MESSAGE, + PREF_WRAP_MESSAGE, } /** @@ -46,6 +49,10 @@ public static String getPreference(Pref pref) { return getPreference(pref.name()); } + public static boolean getPreference(PrefBoolean pref) { + return getPreference(pref, false); + } + public static boolean getPreference(PrefBoolean pref, boolean defaultValue) { return getPreferenceBool(pref.name(), defaultValue); } @@ -66,6 +73,16 @@ public static void setPreference(PrefInt key, int value) { setPreference(key.name(), value); } + public static boolean togglePreference(PrefBoolean prefBoolean) { + return togglePreference(prefBoolean, false); + } + + public static boolean togglePreference(PrefBoolean prefBoolean, boolean defaultValue) { + boolean toggleValue = !getPreference(prefBoolean, defaultValue); + setPreference(prefBoolean, toggleValue); + return toggleValue; + } + public static void resetAll() { Preferences preferences = Preferences.userRoot(); try { diff --git a/src/main/java/com/jpage4500/devicemanager/utils/TextUtils.java b/src/main/java/com/jpage4500/devicemanager/utils/TextUtils.java index 0928760..38dca1b 100644 --- a/src/main/java/com/jpage4500/devicemanager/utils/TextUtils.java +++ b/src/main/java/com/jpage4500/devicemanager/utils/TextUtils.java @@ -430,29 +430,22 @@ public static String formatJson(String text) { tabCount--; trimEnd(sb); sb.append("\n"); - addTabs(sb, tabCount); sb.append(ch); - if (tabCount > 0) { - sb.append("\n"); - addTabs(sb, tabCount); - } } else if (ch == '[') { sb.append("\n"); addTabs(sb, tabCount); sb.append(ch); sb.append("\n"); tabCount++; - addTabs(sb, tabCount); } else if (ch == ']') { tabCount--; trimEnd(sb); sb.append("\n"); addTabs(sb, tabCount); sb.append(ch); - sb.append("\n"); - addTabs(sb, tabCount); } else if (ch == ',') { + sb.append(ch); trimEnd(sb); sb.append("\n"); addTabs(sb, tabCount); @@ -460,13 +453,7 @@ public static String formatJson(String text) { sb.append(" : "); } else if ((ch == ' ' || ch == '\t')) { // discard extra spaces - if (tabCount == 0) { - sb.append(ch); - } } else if ((ch == '\n' || ch == '\r')) { - if (tabCount == 0) { - sb.append(ch); - } // discard extra CR and LF } else { sb.append(ch); @@ -477,6 +464,23 @@ public static String formatJson(String text) { return sb.toString(); } + /** + * check if text string contains valid JSON data + * NOTE: very simple logic.. if a single set of "{}" or "[]" are found - return true + */ + public static boolean containsJson(String text) { + boolean openParagraph = false; + boolean openBracket = false; + for (int i = 0; i < text.length(); i++) { + char ch = text.charAt(i); + if (ch == '{') openParagraph = true; + else if (ch == '[') openBracket = true; + else if (ch == '}' && openParagraph) return true; + else if (ch == ']' && openBracket) return true; + } + return false; + } + private static void trimEnd(StringBuilder sb) { // trim newline or space from end char lastChar = sb.charAt(sb.length() - 1); From bb17ff474ef16596d59d2d80c8fcabfcfe4a19d7 Mon Sep 17 00:00:00 2001 From: Joe Page Date: Sat, 15 Jun 2024 17:52:29 -0400 Subject: [PATCH 04/11] - CMD+= and CMD+- to change font size on log viewer - update libraries - update Log4j to 2.x --- pom.xml | 15 ++--- .../devicemanager/MainApplication.java | 7 +-- .../logging/AndroidLoggerService.java | 48 ++++++++++++++ .../table/utils/LogsCellRenderer.java | 11 ++++ .../devicemanager/ui/LogsScreen.java | 62 +++++++++++++++++-- .../devicemanager/utils/PreferenceUtils.java | 1 + .../org.slf4j.spi.SLF4JServiceProvider | 1 + 7 files changed, 126 insertions(+), 19 deletions(-) create mode 100644 src/main/java/com/jpage4500/devicemanager/logging/AndroidLoggerService.java create mode 100644 src/main/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider diff --git a/pom.xml b/pom.xml index 6e33a91..365ed65 100644 --- a/pom.xml +++ b/pom.xml @@ -21,25 +21,25 @@ com.formdev flatlaf - 2.6 + 3.4.1 org.slf4j slf4j-api - 1.7.36 + 2.0.13 com.google.code.gson gson - 2.10 + 2.11.0 net.coobird thumbnailator - 0.4.17 + 0.4.20 @@ -56,7 +56,7 @@ org.apache.httpcomponents httpclient - 4.5.13 + 4.5.14 @@ -72,7 +72,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.11.0 + 3.13.0 17 17 @@ -80,6 +80,7 @@ maven-assembly-plugin + 3.7.1 package @@ -104,7 +105,7 @@ org.codehaus.mojo exec-maven-plugin - 1.2.1 + 3.3.0 com.jpage4500.devicemanager.MainApplication diff --git a/src/main/java/com/jpage4500/devicemanager/MainApplication.java b/src/main/java/com/jpage4500/devicemanager/MainApplication.java index e6d6876..94ffd11 100644 --- a/src/main/java/com/jpage4500/devicemanager/MainApplication.java +++ b/src/main/java/com/jpage4500/devicemanager/MainApplication.java @@ -62,12 +62,7 @@ private void setupLogging() { } private void initializeUI() { - try { - UIManager.setLookAndFeel(new FlatLightLaf()); - } catch (Exception e) { - log.error("initializeUI: {}", e.getMessage()); - } - + FlatLightLaf.setup(); UIDefaults defaults = UIManager.getLookAndFeelDefaults(); defaults.put("defaultFont", new Font("Arial", Font.PLAIN, 16)); diff --git a/src/main/java/com/jpage4500/devicemanager/logging/AndroidLoggerService.java b/src/main/java/com/jpage4500/devicemanager/logging/AndroidLoggerService.java new file mode 100644 index 0000000..a6350b6 --- /dev/null +++ b/src/main/java/com/jpage4500/devicemanager/logging/AndroidLoggerService.java @@ -0,0 +1,48 @@ +package com.jpage4500.devicemanager.logging; + +import org.slf4j.ILoggerFactory; +import org.slf4j.IMarkerFactory; +import org.slf4j.helpers.BasicMarkerFactory; +import org.slf4j.helpers.NOPMDCAdapter; +import org.slf4j.spi.MDCAdapter; +import org.slf4j.spi.SLF4JServiceProvider; + +public class AndroidLoggerService implements SLF4JServiceProvider { + /** + * Declare the version of the SLF4J API this implementation is compiled against. + * The value of this field is modified with each major release. + */ + // to avoid constant folding by the compiler, this field must *not* be final + public static String REQUESTED_API_VERSION = "2.0.99"; // !final + + private ILoggerFactory loggerFactory; + private IMarkerFactory markerFactory; + private MDCAdapter mdcAdapter; + + public ILoggerFactory getLoggerFactory() { + return loggerFactory; + } + + @Override + public IMarkerFactory getMarkerFactory() { + return markerFactory; + } + + @Override + public MDCAdapter getMDCAdapter() { + return mdcAdapter; + } + + @Override + public String getRequestedApiVersion() { + return REQUESTED_API_VERSION; + } + + @Override + public void initialize() { + loggerFactory = new AppLoggerFactory(); + markerFactory = new BasicMarkerFactory(); + mdcAdapter = new NOPMDCAdapter(); + } + +} diff --git a/src/main/java/com/jpage4500/devicemanager/table/utils/LogsCellRenderer.java b/src/main/java/com/jpage4500/devicemanager/table/utils/LogsCellRenderer.java index bfa6bb0..6430dde 100644 --- a/src/main/java/com/jpage4500/devicemanager/table/utils/LogsCellRenderer.java +++ b/src/main/java/com/jpage4500/devicemanager/table/utils/LogsCellRenderer.java @@ -2,6 +2,7 @@ import com.jpage4500.devicemanager.data.LogEntry; import com.jpage4500.devicemanager.table.LogsTableModel; +import com.jpage4500.devicemanager.utils.PreferenceUtils; import com.jpage4500.devicemanager.utils.TextUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,11 +29,16 @@ public class LogsCellRenderer extends JTextField implements TableCellRenderer { private Highlighter.HighlightPainter highlightPainter2; private boolean isHighlighted = false; + private int fontSize; + public LogsCellRenderer() { setOpaque(true); setEditable(false); Border border = new EmptyBorder(0, 10, 0, 0); setBorder(border); + + fontSize = getFont().getSize(); + notifyFontChanged(); } public Component getTableCellRendererComponent(JTable table, Object object, boolean isSelected, boolean hasFocus, int row, int column) { @@ -96,4 +102,9 @@ public Component getTableCellRendererComponent(JTable table, Object object, bool return this; } + + public void notifyFontChanged() { + int fontOffset = PreferenceUtils.getPreference(PreferenceUtils.PrefInt.PREF_FONT_SIZE_OFFSET, 0); + setFont(getFont().deriveFont(Font.PLAIN, fontOffset + fontSize)); + } } \ No newline at end of file diff --git a/src/main/java/com/jpage4500/devicemanager/ui/LogsScreen.java b/src/main/java/com/jpage4500/devicemanager/ui/LogsScreen.java index be808d1..6dd53d5 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/LogsScreen.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/LogsScreen.java @@ -22,6 +22,7 @@ import javax.swing.*; import javax.swing.border.EmptyBorder; +import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumnModel; import java.awt.*; import java.awt.datatransfer.Clipboard; @@ -201,33 +202,62 @@ private void setupMenuBar() { scrollToFollow(); }); + // [CMD + K] = clear logs + createCmdAction(logsMenu, "Clear logs", KeyEvent.VK_K, e -> model.clearLogs()); + // [CMD + KEY_UP] = scroll to top createCmdAction(logsMenu, "Scoll to top", KeyEvent.VK_UP, e -> { autoScrollCheckBox.setSelected(false); table.scrollToTop(); }); + JMenu editMenu = new JMenu("Edit"); + // [CMD + KEY_DOWN] = scroll to bottom - createCmdAction(logsMenu, "Scoll to bottom", KeyEvent.VK_DOWN, e -> table.scrollToBottom()); + createCmdAction(editMenu, "Scoll to bottom", KeyEvent.VK_DOWN, e -> table.scrollToBottom()); // [CMD + KEY_UP] = page up - createOptionAction(logsMenu, "Page Up", KeyEvent.VK_UP, e -> table.pageUp()); + createOptionAction(editMenu, "Page Up", KeyEvent.VK_UP, e -> table.pageUp()); // [CMD + KE_DOWN] = page down - createOptionAction(logsMenu, "Page Down", KeyEvent.VK_DOWN, e -> table.pageDown()); + createOptionAction(editMenu, "Page Down", KeyEvent.VK_DOWN, e -> table.pageDown()); - // [CMD + K] = clear logs - createCmdAction(logsMenu, "Clear logs", KeyEvent.VK_K, e -> model.clearLogs()); + // [CMD + +] = increase font size + createCmdAction(editMenu, "Increase Font Size", KeyEvent.VK_EQUALS, e -> increaseFontSize()); + + // [CMD + -] = increase font size + createCmdAction(editMenu, "Decrease Font Size", KeyEvent.VK_MINUS, e -> decreaseFontSize()); // [CMD + F] = focus search field - createCmdAction(logsMenu, "Search for...", KeyEvent.VK_F, e -> searchField.requestFocus()); + createCmdAction(editMenu, "Search for...", KeyEvent.VK_F, e -> searchField.requestFocus()); JMenuBar menubar = new JMenuBar(); menubar.add(windowMenu); + menubar.add(editMenu); menubar.add(logsMenu); setJMenuBar(menubar); } + public void increaseFontSize() { + int fontOffset = PreferenceUtils.getPreference(PreferenceUtils.PrefInt.PREF_FONT_SIZE_OFFSET, 0); + fontOffset++; + setFontSize(fontOffset); + } + + private void setFontSize(int fontOffset) { + PreferenceUtils.setPreference(PreferenceUtils.PrefInt.PREF_FONT_SIZE_OFFSET, fontOffset); + + LogsCellRenderer cellRenderer = (LogsCellRenderer) table.getDefaultRenderer(LogEntry.class); + cellRenderer.notifyFontChanged(); + model.fireTableDataChanged(); + } + + public void decreaseFontSize() { + int fontOffset = PreferenceUtils.getPreference(PreferenceUtils.PrefInt.PREF_FONT_SIZE_OFFSET, 0); + fontOffset--; + setFontSize(fontOffset); + } + private void closeWindow() { log.trace("closeWindow: {}", device.getDisplayName()); // save last filter @@ -283,6 +313,26 @@ public void actionPerformed(ActionEvent e) { } }); +// // CMD+PLUS -> inceaase font +// KeyStroke increaseFont = KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, InputEvent.META_DOWN_MASK); +// table.getInputMap(JTable.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(increaseFont, "Increase Font Size"); +// table.getActionMap().put("Increase Font Size", new AbstractAction() { +// @Override +// public void actionPerformed(ActionEvent e) { +// increaseFontSize(); +// } +// }); +// +// // CMD+MINUS -> decrease font +// KeyStroke decreaseFont = KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, InputEvent.META_DOWN_MASK); +// table.getInputMap(JTable.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(decreaseFont, "Decrease Font Size"); +// table.getActionMap().put("Decrease Font Size", new AbstractAction() { +// @Override +// public void actionPerformed(ActionEvent e) { +// decreaseFontSize(); +// } +// }); + table.getSelectionModel().addListSelectionListener(event -> { if (event.getValueIsAdjusting()) return; diff --git a/src/main/java/com/jpage4500/devicemanager/utils/PreferenceUtils.java b/src/main/java/com/jpage4500/devicemanager/utils/PreferenceUtils.java index 44310aa..ebd5e25 100644 --- a/src/main/java/com/jpage4500/devicemanager/utils/PreferenceUtils.java +++ b/src/main/java/com/jpage4500/devicemanager/utils/PreferenceUtils.java @@ -43,6 +43,7 @@ public enum PrefBoolean { */ public enum PrefInt { PREF_LAST_DEVICE_PORT, + PREF_FONT_SIZE_OFFSET, } public static String getPreference(Pref pref) { diff --git a/src/main/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider b/src/main/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider new file mode 100644 index 0000000..1e71be9 --- /dev/null +++ b/src/main/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider @@ -0,0 +1 @@ +com.jpage4500.devicemanager.logging.AndroidLoggerService From fad66a24ec4a8407423326b03464600515538f77 Mon Sep 17 00:00:00 2001 From: Joe Page Date: Tue, 18 Jun 2024 18:00:45 -0400 Subject: [PATCH 05/11] - popup menu for disconnected devices --- .../devicemanager/data/LogFilter.java | 4 +- .../devicemanager/manager/DeviceManager.java | 5 +- .../devicemanager/table/DeviceTableModel.java | 25 ++-- .../table/utils/LogsCellRenderer.java | 6 +- .../devicemanager/ui/DeviceScreen.java | 130 ++++++++++++------ .../ui/dialog/AddFilterDialog.java | 18 ++- .../devicemanager/utils/FileUtils.java | 3 +- 7 files changed, 128 insertions(+), 63 deletions(-) diff --git a/src/main/java/com/jpage4500/devicemanager/data/LogFilter.java b/src/main/java/com/jpage4500/devicemanager/data/LogFilter.java index bfa1801..28fda6c 100644 --- a/src/main/java/com/jpage4500/devicemanager/data/LogFilter.java +++ b/src/main/java/com/jpage4500/devicemanager/data/LogFilter.java @@ -66,8 +66,8 @@ public boolean isMatch(LogEntry logEntry) { case PID -> logValue = logEntry.pid; case LEVEL -> { logValue = logEntry.level; - if (value != null && expression == Expression.ENDS_WITH) { - //log.trace("isMatch: {}", logValue); + //log.trace("isMatch: {}, val:{}, expr:{}", logValue, value, expression); + if (value != null && expression == Expression.STARTS_WITH) { switch (value) { case "D": return TextUtils.equalsIgnoreCaseAny(logValue, "D", "I", "W", "E"); diff --git a/src/main/java/com/jpage4500/devicemanager/manager/DeviceManager.java b/src/main/java/com/jpage4500/devicemanager/manager/DeviceManager.java index 1785d67..8bc5068 100644 --- a/src/main/java/com/jpage4500/devicemanager/manager/DeviceManager.java +++ b/src/main/java/com/jpage4500/devicemanager/manager/DeviceManager.java @@ -208,7 +208,7 @@ private void handleDeviceUpdate(List devices, DeviceListener listene // run periodic task to update device state if (deviceRefreshRuture == null) { deviceRefreshRuture = scheduledExecutorService.scheduleWithFixedDelay(() -> { - log.trace("handleDeviceUpdate: REFRESH"); + //log.trace("handleDeviceUpdate: REFRESH"); for (Device device : deviceList) { if (device.isOnline) { fetchDeviceDetails(device, false, listener); @@ -272,9 +272,8 @@ private void fetchDeviceDetails(Device device, boolean fullRefresh, DeviceListen device.lastUpdateMs = System.currentTimeMillis(); - if (log.isTraceEnabled()) log.trace("fetchDeviceDetails: {}: full:{}, {}", timer, fullRefresh, GsonHelper.toJson(device)); - if (fullRefresh) { + if (log.isTraceEnabled()) log.trace("fetchDeviceDetails: {}: full:{}, {}", timer, fullRefresh, GsonHelper.toJson(device)); // keep track of wireless devices ConnectDialog.addWirelessDevice(device); } diff --git a/src/main/java/com/jpage4500/devicemanager/table/DeviceTableModel.java b/src/main/java/com/jpage4500/devicemanager/table/DeviceTableModel.java index 1e898cb..aaff0d9 100644 --- a/src/main/java/com/jpage4500/devicemanager/table/DeviceTableModel.java +++ b/src/main/java/com/jpage4500/devicemanager/table/DeviceTableModel.java @@ -41,10 +41,6 @@ public void setDeviceList(List deviceList) { fireTableDataChanged(); } - public List getDeviceList() { - return deviceList; - } - public void setHiddenColumns(List hiddenColumns) { Columns[] columns = Columns.values(); int numColumns = columns.length; @@ -84,15 +80,28 @@ public Columns getColumnType(int colIndex) { return null; } - public void updateRowForDevice(Device device) { - if (device == null) return; + public void updateDevice(Device device) { + int row = getRowForDevice(device); + if (row >= 0) fireTableRowsUpdated(row, row); + } + + public void removeDevice(Device device) { + int row = getRowForDevice(device); + if (row >= 0) { + deviceList.remove(row); + fireTableRowsDeleted(row, row); + } + } + + public int getRowForDevice(Device device) { + if (device == null) return -1; for (int row = 0; row < deviceList.size(); row++) { Device d = deviceList.get(row); if (TextUtils.equals(d.serial, device.serial)) { - fireTableRowsUpdated(row, row); - break; + return row; } } + return -1; } public void setAppList(List appList) { diff --git a/src/main/java/com/jpage4500/devicemanager/table/utils/LogsCellRenderer.java b/src/main/java/com/jpage4500/devicemanager/table/utils/LogsCellRenderer.java index 6430dde..d5187ee 100644 --- a/src/main/java/com/jpage4500/devicemanager/table/utils/LogsCellRenderer.java +++ b/src/main/java/com/jpage4500/devicemanager/table/utils/LogsCellRenderer.java @@ -29,7 +29,7 @@ public class LogsCellRenderer extends JTextField implements TableCellRenderer { private Highlighter.HighlightPainter highlightPainter2; private boolean isHighlighted = false; - private int fontSize; + private final int defaultFontSize; public LogsCellRenderer() { setOpaque(true); @@ -37,7 +37,7 @@ public LogsCellRenderer() { Border border = new EmptyBorder(0, 10, 0, 0); setBorder(border); - fontSize = getFont().getSize(); + defaultFontSize = getFont().getSize(); notifyFontChanged(); } @@ -105,6 +105,6 @@ public Component getTableCellRendererComponent(JTable table, Object object, bool public void notifyFontChanged() { int fontOffset = PreferenceUtils.getPreference(PreferenceUtils.PrefInt.PREF_FONT_SIZE_OFFSET, 0); - setFont(getFont().deriveFont(Font.PLAIN, fontOffset + fontSize)); + setFont(getFont().deriveFont(Font.PLAIN, fontOffset + defaultFontSize)); } } \ No newline at end of file diff --git a/src/main/java/com/jpage4500/devicemanager/ui/DeviceScreen.java b/src/main/java/com/jpage4500/devicemanager/ui/DeviceScreen.java index aec95ff..637a75b 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/DeviceScreen.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/DeviceScreen.java @@ -100,7 +100,7 @@ public void handleDevicesUpdated(List deviceList) { @Override public void handleDeviceUpdated(Device device) { SwingUtilities.invokeLater(() -> { - model.updateRowForDevice(device); + model.updateDevice(device); updateDeviceState(device); sorter.sort(); }); @@ -258,29 +258,59 @@ private void setupTable() { new DropTarget(table, new FileDragAndDropListener(table, this::handleFilesDropped)); table.setPopupMenuListener((row, column) -> { - if (row == -1) { - JPopupMenu popupMenu = new JPopupMenu(); - DeviceTableModel.Columns columnType = model.getColumnType(column); - if (columnType != null) { - JMenuItem hideItem = new JMenuItem("Hide Column " + columnType.name()); - hideItem.addActionListener(actionEvent -> handleHideColumn(column)); - popupMenu.add(hideItem); - - JMenuItem sizeToFitItem = new JMenuItem("Size to Fit"); - sizeToFitItem.addActionListener(actionEvent -> { - TableColumnAdjuster adjuster = new TableColumnAdjuster(table, 0); - adjuster.adjustColumn(column); - }); - popupMenu.add(sizeToFitItem); - return popupMenu; - } - return null; + return getPopupMenu(row, column); + }); + + table.setTooltipListener((row, col) -> { + int modelCol = table.convertColumnIndexToModel(col); + DeviceTableModel.Columns columnType = model.getColumnType(modelCol); + if (row >= 0 && columnType == DeviceTableModel.Columns.BATTERY) { + // always show battery level and power status in tooltip + int modelRow = table.convertRowIndexToModel(row); + Device device = (Device) model.getValueAt(modelRow, modelCol); + String tooltip = device.batteryLevel + "%"; + if (device.powerStatus != Device.PowerStatus.POWER_NONE) tooltip += " (" + device.powerStatus + ")"; + return tooltip; + } else { + return table.getTextIfTruncated(row, col); } - Device device = model.getDeviceAtRow(row); - if (device == null) return null; + }); + + table.getSelectionModel().addListSelectionListener(e -> { + if (e.getValueIsAdjusting()) return; + refreshUi(); + }); + } + /** + * @return PopupMenu to display or null + */ + private JPopupMenu getPopupMenu(int row, int column) { + if (row == -1) { + // header JPopupMenu popupMenu = new JPopupMenu(); + DeviceTableModel.Columns columnType = model.getColumnType(column); + if (columnType != null) { + JMenuItem hideItem = new JMenuItem("Hide Column " + columnType.name()); + hideItem.addActionListener(actionEvent -> handleHideColumn(column)); + popupMenu.add(hideItem); + + JMenuItem sizeToFitItem = new JMenuItem("Size to Fit"); + sizeToFitItem.addActionListener(actionEvent -> { + TableColumnAdjuster adjuster = new TableColumnAdjuster(table, 0); + adjuster.adjustColumn(column); + }); + popupMenu.add(sizeToFitItem); + return popupMenu; + } + return null; + } + Device device = model.getDeviceAtRow(row); + if (device == null) return null; + JPopupMenu popupMenu = new JPopupMenu(); + + if (device.isOnline) { JMenuItem copyFieldItem = new JMenuItem("Copy Field to Clipboard"); copyFieldItem.addActionListener(actionEvent -> handleCopyClipboardFieldCommand()); popupMenu.add(copyFieldItem); @@ -325,28 +355,18 @@ private void setupTable() { disconnectItem.addActionListener(actionEvent -> handleDisconnect(device)); popupMenu.add(disconnectItem); } - return popupMenu; - }); - - table.setTooltipListener((row, col) -> { - int modelCol = table.convertColumnIndexToModel(col); - DeviceTableModel.Columns columnType = model.getColumnType(modelCol); - if (row >= 0 && columnType == DeviceTableModel.Columns.BATTERY) { - // always show battery level and power status in tooltip - int modelRow = table.convertRowIndexToModel(row); - Device device = (Device) model.getValueAt(modelRow, modelCol); - String tooltip = device.batteryLevel + "%"; - if (device.powerStatus != Device.PowerStatus.POWER_NONE) tooltip += " (" + device.powerStatus + ")"; - return tooltip; - } else { - return table.getTextIfTruncated(row, col); + } else { + // offline device + if (device.isWireless()) { + JMenuItem reconnectItem = new JMenuItem("Reconnect"); + reconnectItem.addActionListener(actionEvent -> handleReconnectDevice(device)); + popupMenu.add(reconnectItem); } - }); - - table.getSelectionModel().addListSelectionListener(e -> { - if (e.getValueIsAdjusting()) return; - refreshUi(); - }); + JMenuItem removeItem = new JMenuItem("Remove"); + removeItem.addActionListener(actionEvent -> handleRemoveDevice(device)); + popupMenu.add(removeItem); + } + return popupMenu; } private void setupSystemTray() { @@ -611,7 +631,7 @@ private void handleSetProperty(int number) { }); device.setCustomProperty(Device.CUSTOM_PROP_X + number, result); - model.updateRowForDevice(device); + model.updateDevice(device); } } @@ -661,8 +681,8 @@ private void setDeviceBusy(Device device, boolean isBusy) { } if (SwingUtilities.isEventDispatchThread()) { - model.updateRowForDevice(device); - } else SwingUtilities.invokeLater(() -> model.updateRowForDevice(device)); + model.updateDevice(device); + } else SwingUtilities.invokeLater(() -> model.updateDevice(device)); } private void handleConnectDevice() { @@ -680,6 +700,28 @@ private void handleDisconnect(Device device) { }); } + private void handleRemoveDevice(Device device) { + model.removeDevice(device); + } + + private void handleReconnectDevice(Device device) { + String[] deviceSplit = device.serial.split(":"); + if (deviceSplit.length < 2) return; + + String ip = deviceSplit[0]; + int port; + try { + port = Integer.parseInt(deviceSplit[1]); + } catch (NumberFormatException e) { + log.error("Invalid port: " + deviceSplit[1]); + return; + } + + DeviceManager.getInstance().connectDevice(ip, port, (isSuccess, error) -> { + if (!isSuccess) JOptionPane.showMessageDialog(this, "Unable to connect!"); + }); + } + private void handleDeviceDetails() { Device device = getFirstSelectedDevice(); if (device == null) return; diff --git a/src/main/java/com/jpage4500/devicemanager/ui/dialog/AddFilterDialog.java b/src/main/java/com/jpage4500/devicemanager/ui/dialog/AddFilterDialog.java index f443e99..592b5ae 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/dialog/AddFilterDialog.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/dialog/AddFilterDialog.java @@ -1,6 +1,7 @@ package com.jpage4500.devicemanager.ui.dialog; import com.jpage4500.devicemanager.data.LogFilter; +import com.jpage4500.devicemanager.table.LogsTableModel; import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -23,22 +24,35 @@ public static LogFilter showAddFilterDialog(Component frame, LogFilter logFilter } public AddFilterDialog(LogFilter logFilter) { - logFilter = logFilter; if (logFilter == null) logFilter = new LogFilter(); + this.logFilter = logFilter; initalizeUi(); } protected void initalizeUi() { - setLayout(new MigLayout("fillx", "[][]")); + setLayout(new MigLayout("fillx", "[][][]")); add(new JLabel("Filter Name")); JTextField nameField = new JTextField(); add(nameField); + // column + LogsTableModel.Columns[] columns = LogsTableModel.Columns.values(); + JComboBox columnComboBox = new JComboBox<>(columns); + add(columnComboBox, ""); + + LogFilter.Expression[] expressions = LogFilter.Expression.values(); + JComboBox expressionComboBox = new JComboBox<>(expressions); + add(expressionComboBox, ""); + + JTextField valueField = new JTextField(); + add(valueField, ""); + JButton addButton = new JButton("Add Filter"); addButton.addActionListener(e -> handleAddClicked()); add(addButton, "al right, span 2, wrap"); + } private void handleAddClicked() { diff --git a/src/main/java/com/jpage4500/devicemanager/utils/FileUtils.java b/src/main/java/com/jpage4500/devicemanager/utils/FileUtils.java index 3fc9ae5..11de66b 100644 --- a/src/main/java/com/jpage4500/devicemanager/utils/FileUtils.java +++ b/src/main/java/com/jpage4500/devicemanager/utils/FileUtils.java @@ -76,7 +76,8 @@ public static String bytesToDisplayString(Long sizeInBytes) { * return string description of number of bytes (in X.X gig) */ public static String bytesToGigDisplayString(Long sizeInBytes) { - if (sizeInBytes == null || sizeInBytes <= 0) return "0.0G"; + if (sizeInBytes == null) return ""; + else if (sizeInBytes <= 0) return "0.0G"; int digitGroups = 3; //(int) (Math.log10(sizeInBytes) / Math.log10(1024)); return sizeGigDisplayFormat.format(sizeInBytes / Math.pow(1024, digitGroups)) + SIZE_UNITS[digitGroups]; From 82d44de464ccbfbd91f75fad6b8c25979e9815a4 Mon Sep 17 00:00:00 2001 From: Joe Page Date: Wed, 19 Jun 2024 16:45:24 -0400 Subject: [PATCH 06/11] - working on add log filter dialog - explorer improvements - better column names - many misc UI changes --- .../devicemanager/data/DeviceFile.java | 4 +- .../devicemanager/data/LogFilter.java | 16 ++++- .../devicemanager/manager/DeviceManager.java | 17 +++-- .../devicemanager/table/DeviceTableModel.java | 31 ++++++--- .../table/ExploreTableModel.java | 19 ++++-- .../devicemanager/table/LogsTableModel.java | 27 +++++--- .../devicemanager/ui/DeviceScreen.java | 30 ++++----- .../devicemanager/ui/ExploreScreen.java | 13 ++-- .../devicemanager/ui/LogsScreen.java | 26 ++++---- .../ui/dialog/AddFilterDialog.java | 64 ++++++++++++++----- .../devicemanager/ui/views/HintTextField.java | 4 +- .../devicemanager/utils/ArrayUtils.java | 35 ++++++++++ 12 files changed, 205 insertions(+), 81 deletions(-) create mode 100644 src/main/java/com/jpage4500/devicemanager/utils/ArrayUtils.java diff --git a/src/main/java/com/jpage4500/devicemanager/data/DeviceFile.java b/src/main/java/com/jpage4500/devicemanager/data/DeviceFile.java index a2a7f1d..565f1f3 100644 --- a/src/main/java/com/jpage4500/devicemanager/data/DeviceFile.java +++ b/src/main/java/com/jpage4500/devicemanager/data/DeviceFile.java @@ -144,7 +144,9 @@ public static DeviceFile fromEntry(String line) { } else { file.isDirectory = true; } - + } + if (TextUtils.equalsIgnoreCaseAny(securityType, "rootfs")) { + file.isReadOnly = true; } return file; } diff --git a/src/main/java/com/jpage4500/devicemanager/data/LogFilter.java b/src/main/java/com/jpage4500/devicemanager/data/LogFilter.java index 28fda6c..ab92644 100644 --- a/src/main/java/com/jpage4500/devicemanager/data/LogFilter.java +++ b/src/main/java/com/jpage4500/devicemanager/data/LogFilter.java @@ -13,7 +13,21 @@ public class LogFilter { List filterList; public enum Expression { - EQUALS, CONTAINS, STARTS_WITH, ENDS_WITH + EQUALS("is"), + CONTAINS("contains"), + STARTS_WITH("starts with"), + ENDS_WITH("ends with"), + ; + String desc; + + Expression(String desc) { + this.desc = desc; + } + + @Override + public String toString() { + return desc; + } } public static class FilterExpression { diff --git a/src/main/java/com/jpage4500/devicemanager/manager/DeviceManager.java b/src/main/java/com/jpage4500/devicemanager/manager/DeviceManager.java index 8bc5068..2069c0d 100644 --- a/src/main/java/com/jpage4500/devicemanager/manager/DeviceManager.java +++ b/src/main/java/com/jpage4500/devicemanager/manager/DeviceManager.java @@ -3,11 +3,12 @@ import com.jpage4500.devicemanager.data.Device; import com.jpage4500.devicemanager.data.DeviceFile; import com.jpage4500.devicemanager.data.LogEntry; -import com.jpage4500.devicemanager.ui.ExploreScreen; import com.jpage4500.devicemanager.ui.dialog.ConnectDialog; import com.jpage4500.devicemanager.ui.dialog.SettingsDialog; -import com.jpage4500.devicemanager.utils.*; +import com.jpage4500.devicemanager.utils.GsonHelper; +import com.jpage4500.devicemanager.utils.TextUtils; import com.jpage4500.devicemanager.utils.Timer; +import com.jpage4500.devicemanager.utils.Utils; import se.vidstige.jadb.*; import se.vidstige.jadb.managers.PackageManager; import se.vidstige.jadb.managers.PropertyManager; @@ -28,7 +29,6 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.jar.JarEntry; import java.util.jar.JarFile; -import java.util.prefs.Preferences; public class DeviceManager { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DeviceManager.class); @@ -636,7 +636,7 @@ public void listFiles(Device device, String path, boolean useRoot, DeviceFileLis if (safePath.indexOf(' ') > 0) { safePath = "\"" + safePath + "\""; } - //log.trace("listFiles: {} {}", safePath, useRoot ? "(ROOT)" : ""); + log.trace("listFiles: {} {}", safePath, useRoot ? "(ROOT)" : ""); String command = "ls -alZ " + safePath; if (useRoot) command = "su -c " + command; ShellResult result = runShell(device, command); @@ -648,12 +648,15 @@ public void listFiles(Device device, String path, boolean useRoot, DeviceFileLis else if (i == 0) { // not a valid file/dir listing; check for known errors if (TextUtils.contains(dir, "su:")) { + log.debug("listFiles: NO_ROOT:{}", dir); listener.handleFiles(null, ERR_ROOT_NOT_AVAILABLE); return; } else if (TextUtils.containsAny(dir, true, "permission denied")) { + log.debug("listFiles: NO_PERMISSION:{}", dir); listener.handleFiles(null, ERR_PERMISSION_DENIED); return; } else if (TextUtils.containsAny(dir, true, "Not a directory", "No such file or directory")) { + log.debug("listFiles: NOT_DIR:{}, {}", dir, GsonHelper.toJson(result.resultList)); listener.handleFiles(null, ERR_NOT_A_DIRECTORY); return; } @@ -874,10 +877,14 @@ private Map getProcessMap(Device device) { // 7677 [csf_sync_update] Map pidMap = new HashMap<>(); for (String line : result.resultList) { - String[] lineArr = line.trim().split(" ", 2); + String[] lineArr = line.trim().split(" "); if (lineArr.length < 2) continue; String pid = lineArr[0]; String app = lineArr[1]; + int atPos = app.indexOf('@'); + if (atPos > 0) { + app = app.substring(0, atPos); + } pidMap.put(pid, app); } return pidMap; diff --git a/src/main/java/com/jpage4500/devicemanager/table/DeviceTableModel.java b/src/main/java/com/jpage4500/devicemanager/table/DeviceTableModel.java index aaff0d9..fac7d58 100644 --- a/src/main/java/com/jpage4500/devicemanager/table/DeviceTableModel.java +++ b/src/main/java/com/jpage4500/devicemanager/table/DeviceTableModel.java @@ -18,15 +18,26 @@ public class DeviceTableModel extends AbstractTableModel { private Columns[] visibleColumns; public enum Columns { - NAME, - SERIAL, - MODEL, - PHONE, - IMEI, - BATTERY, - FREE, - CUSTOM1, - CUSTOM2, + NAME("Name"), + SERIAL("Serial"), + MODEL("Model"), + PHONE("Phone"), + IMEI("IMEI"), + BATTERY("Battery"), + FREE("Free"), + CUSTOM1("Custom 1"), + CUSTOM2("Custom 2"), + ; + String desc; + + Columns(String desc) { + this.desc = desc; + } + + @Override + public String toString() { + return desc; + } } public DeviceTableModel() { @@ -125,7 +136,7 @@ public Class getColumnClass(int columnIndex) { public String getColumnName(int i) { if (i < visibleColumns.length) { Columns colType = visibleColumns[i]; - return colType.name(); + return colType.toString(); } else { return appList.get(i - visibleColumns.length); } diff --git a/src/main/java/com/jpage4500/devicemanager/table/ExploreTableModel.java b/src/main/java/com/jpage4500/devicemanager/table/ExploreTableModel.java index fa87f0f..0525d16 100644 --- a/src/main/java/com/jpage4500/devicemanager/table/ExploreTableModel.java +++ b/src/main/java/com/jpage4500/devicemanager/table/ExploreTableModel.java @@ -14,9 +14,20 @@ public class ExploreTableModel extends AbstractTableModel { private final List fileList; public enum Columns { - NAME, - SIZE, - DATE, + NAME("Name"), + SIZE("Size"), + DATE("Date"), + ; + String desc; + + Columns(String desc) { + this.desc = desc; + } + + @Override + public String toString() { + return desc; + } } public ExploreTableModel() { @@ -53,7 +64,7 @@ public Class getColumnClass(int columnIndex) { public String getColumnName(int i) { Columns[] columns = Columns.values(); Columns colType = columns[i]; - return colType.name(); + return colType.toString(); } public int getRowCount() { diff --git a/src/main/java/com/jpage4500/devicemanager/table/LogsTableModel.java b/src/main/java/com/jpage4500/devicemanager/table/LogsTableModel.java index ce42bc0..0061d31 100644 --- a/src/main/java/com/jpage4500/devicemanager/table/LogsTableModel.java +++ b/src/main/java/com/jpage4500/devicemanager/table/LogsTableModel.java @@ -48,13 +48,24 @@ public String getTextValue(int row, int column) { } public enum Columns { - DATE, - APP, - TID, - PID, - LEVEL, - TAG, - MSG, + DATE("Date"), + APP("App"), + TID("TID"), + PID("PID"), + LEVEL("Level"), + TAG("Tag"), + MSG("Message"), + ; + String desc; + + Columns(String desc) { + this.desc = desc; + } + + @Override + public String toString() { + return desc; + } } public LogsTableModel() { @@ -133,7 +144,7 @@ public String getColumnName(int i) { Columns[] columns = Columns.values(); if (i < columns.length) { Columns colType = columns[i]; - return colType.name(); + return colType.toString(); } return null; } diff --git a/src/main/java/com/jpage4500/devicemanager/ui/DeviceScreen.java b/src/main/java/com/jpage4500/devicemanager/ui/DeviceScreen.java index 637a75b..8f6a105 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/DeviceScreen.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/DeviceScreen.java @@ -225,19 +225,19 @@ private void setupTable() { table.setDefaultRenderer(Device.class, new DeviceCellRenderer()); table.setEmptyText("No Android Devices!"); + // use some default column sizes + TableColumnModel columnModel = table.getColumnModel(); + columnModel.getColumn(DeviceTableModel.Columns.NAME.ordinal()).setPreferredWidth(185); + columnModel.getColumn(DeviceTableModel.Columns.SERIAL.ordinal()).setPreferredWidth(152); + columnModel.getColumn(DeviceTableModel.Columns.PHONE.ordinal()).setPreferredWidth(116); + columnModel.getColumn(DeviceTableModel.Columns.IMEI.ordinal()).setPreferredWidth(147); + columnModel.getColumn(DeviceTableModel.Columns.BATTERY.ordinal()).setPreferredWidth(31); + columnModel.getColumn(DeviceTableModel.Columns.BATTERY.ordinal()).setMaxWidth(31); + columnModel.getColumn(DeviceTableModel.Columns.FREE.ordinal()).setPreferredWidth(66); + columnModel.getColumn(DeviceTableModel.Columns.FREE.ordinal()).setMaxWidth(80); + // restore user-defined column sizes - if (!table.restore()) { - // first-time running - use some default column sizes - TableColumnModel columnModel = table.getColumnModel(); - columnModel.getColumn(DeviceTableModel.Columns.NAME.ordinal()).setPreferredWidth(185); - columnModel.getColumn(DeviceTableModel.Columns.SERIAL.ordinal()).setPreferredWidth(152); - columnModel.getColumn(DeviceTableModel.Columns.PHONE.ordinal()).setPreferredWidth(116); - columnModel.getColumn(DeviceTableModel.Columns.IMEI.ordinal()).setPreferredWidth(147); - columnModel.getColumn(DeviceTableModel.Columns.BATTERY.ordinal()).setPreferredWidth(31); - columnModel.getColumn(DeviceTableModel.Columns.BATTERY.ordinal()).setMaxWidth(31); - columnModel.getColumn(DeviceTableModel.Columns.FREE.ordinal()).setPreferredWidth(66); - columnModel.getColumn(DeviceTableModel.Columns.FREE.ordinal()).setMaxWidth(80); - } + table.restore(); sorter = new DeviceRowSorter(model); table.setRowSorter(sorter); @@ -257,9 +257,7 @@ private void setupTable() { // support drag and drop of files IN TO deviceView new DropTarget(table, new FileDragAndDropListener(table, this::handleFilesDropped)); - table.setPopupMenuListener((row, column) -> { - return getPopupMenu(row, column); - }); + table.setPopupMenuListener((row, column) -> getPopupMenu(row, column)); table.setTooltipListener((row, col) -> { int modelCol = table.convertColumnIndexToModel(col); @@ -701,6 +699,7 @@ private void handleDisconnect(Device device) { } private void handleRemoveDevice(Device device) { + log.debug("handleRemoveDevice: {}", device.serial); model.removeDevice(device); } @@ -812,7 +811,6 @@ private List getSelectedDevices() { private void setupToolbar() { toolbar.setRollover(true); - JButton button; createToolbarButton(toolbar, "icon_add.png", "Connect", "Connect Device", actionEvent -> handleConnectDevice()); toolbar.addSeparator(); diff --git a/src/main/java/com/jpage4500/devicemanager/ui/ExploreScreen.java b/src/main/java/com/jpage4500/devicemanager/ui/ExploreScreen.java index 61d89b2..d24267d 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/ExploreScreen.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/ExploreScreen.java @@ -296,15 +296,20 @@ private void refreshFiles() { DeviceManager.getInstance().listFiles(device, selectedPath, useRoot, (fileList, error) -> SwingUtilities.invokeLater(() -> { if (error != null) { errorMessage = error; + boolean doRefresh = false; if (useRoot && TextUtils.equals(error, DeviceManager.ERR_ROOT_NOT_AVAILABLE)) { JOptionPane.showMessageDialog(this, "ROOT not available!"); toggleRoot(); - } else if (TextUtils.equals(error, DeviceManager.ERR_PERMISSION_DENIED)) { - errorMessage = "permission denied"; - // revert to previous directory - setPath(null); + } else if (TextUtils.equals(error, DeviceManager.ERR_NOT_A_DIRECTORY)) { + if (prevPathList.isEmpty() && TextUtils.equals(selectedPath, "/sdcard")) { + // some devices don't allow browsing /sdcard (Samsung S10) -- + doRefresh = true; + } } + // revert to previous directory + setPath(null); refreshUi(); + if (doRefresh) refreshFiles(); return; } if (fileList == null) { diff --git a/src/main/java/com/jpage4500/devicemanager/ui/LogsScreen.java b/src/main/java/com/jpage4500/devicemanager/ui/LogsScreen.java index 6dd53d5..c08fb0c 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/LogsScreen.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/LogsScreen.java @@ -278,20 +278,20 @@ private void setupTable() { table.setModel(model); table.setDefaultRenderer(LogEntry.class, new LogsCellRenderer()); + // default column sizes + TableColumnModel columnModel = table.getColumnModel(); + columnModel.getColumn(LogsTableModel.Columns.LEVEL.ordinal()).setPreferredWidth(28); + columnModel.getColumn(LogsTableModel.Columns.LEVEL.ordinal()).setMaxWidth(35); + columnModel.getColumn(LogsTableModel.Columns.PID.ordinal()).setPreferredWidth(60); + columnModel.getColumn(LogsTableModel.Columns.PID.ordinal()).setMaxWidth(100); + columnModel.getColumn(LogsTableModel.Columns.TID.ordinal()).setPreferredWidth(60); + columnModel.getColumn(LogsTableModel.Columns.TID.ordinal()).setMaxWidth(100); + columnModel.getColumn(LogsTableModel.Columns.DATE.ordinal()).setPreferredWidth(159); + columnModel.getColumn(LogsTableModel.Columns.APP.ordinal()).setPreferredWidth(150); + columnModel.getColumn(LogsTableModel.Columns.MSG.ordinal()).setPreferredWidth(700); + // restore user-defined column sizes - if (!table.restore()) { - // default column sizes - TableColumnModel columnModel = table.getColumnModel(); - columnModel.getColumn(LogsTableModel.Columns.LEVEL.ordinal()).setPreferredWidth(28); - columnModel.getColumn(LogsTableModel.Columns.LEVEL.ordinal()).setMaxWidth(35); - columnModel.getColumn(LogsTableModel.Columns.PID.ordinal()).setPreferredWidth(60); - columnModel.getColumn(LogsTableModel.Columns.PID.ordinal()).setMaxWidth(100); - columnModel.getColumn(LogsTableModel.Columns.TID.ordinal()).setPreferredWidth(60); - columnModel.getColumn(LogsTableModel.Columns.TID.ordinal()).setMaxWidth(100); - columnModel.getColumn(LogsTableModel.Columns.DATE.ordinal()).setPreferredWidth(159); - columnModel.getColumn(LogsTableModel.Columns.APP.ordinal()).setPreferredWidth(150); - columnModel.getColumn(LogsTableModel.Columns.MSG.ordinal()).setPreferredWidth(700); - } + table.restore(); // ENTER -> view message KeyStroke enter = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0); diff --git a/src/main/java/com/jpage4500/devicemanager/ui/dialog/AddFilterDialog.java b/src/main/java/com/jpage4500/devicemanager/ui/dialog/AddFilterDialog.java index 592b5ae..6e65caf 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/dialog/AddFilterDialog.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/dialog/AddFilterDialog.java @@ -2,62 +2,92 @@ import com.jpage4500.devicemanager.data.LogFilter; import com.jpage4500.devicemanager.table.LogsTableModel; +import com.jpage4500.devicemanager.ui.views.HintTextField; +import com.jpage4500.devicemanager.utils.ArrayUtils; import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.swing.*; import java.awt.*; +import java.util.ArrayList; +import java.util.List; public class AddFilterDialog extends JPanel { private static final Logger log = LoggerFactory.getLogger(AddFilterDialog.class); private LogFilter logFilter; + private HintTextField nameTextField; + private List filterList; public static LogFilter showAddFilterDialog(Component frame, LogFilter logFilter) { + String okButton = logFilter == null ? "Save" : "Update"; AddFilterDialog screen = new AddFilterDialog(logFilter); int rc = JOptionPane.showOptionDialog(frame, screen, "Add Filter", JOptionPane.DEFAULT_OPTION, - JOptionPane.PLAIN_MESSAGE, null, new Object[]{}, null); + JOptionPane.PLAIN_MESSAGE, null, new String[]{okButton, "Cancel"}, null); if (rc != JOptionPane.YES_OPTION) return null; + // SAVE/UPDATE filter + return screen.logFilter; } public AddFilterDialog(LogFilter logFilter) { if (logFilter == null) logFilter = new LogFilter(); this.logFilter = logFilter; + filterList = new ArrayList<>(); initalizeUi(); } protected void initalizeUi() { - setLayout(new MigLayout("fillx", "[][][]")); - - add(new JLabel("Filter Name")); - JTextField nameField = new JTextField(); - add(nameField); + setLayout(new MigLayout("fillx", "[][][][]")); - // column - LogsTableModel.Columns[] columns = LogsTableModel.Columns.values(); - JComboBox columnComboBox = new JComboBox<>(columns); - add(columnComboBox, ""); + nameTextField = new HintTextField("Filter Name", null); + add(nameTextField, "span 4, grow, wrap 2"); - LogFilter.Expression[] expressions = LogFilter.Expression.values(); - JComboBox expressionComboBox = new JComboBox<>(expressions); - add(expressionComboBox, ""); - - JTextField valueField = new JTextField(); - add(valueField, ""); + addFilter(); + // add/update filter JButton addButton = new JButton("Add Filter"); addButton.addActionListener(e -> handleAddClicked()); - add(addButton, "al right, span 2, wrap"); + add(addButton, "skip 2, wrap"); + } + private void addFilter() { + FilterPanel filterPanel = new FilterPanel(); + + add(filterPanel.columnComboBox, ""); + add(filterPanel.expressionComboBox, ""); + add(filterPanel.valueField, "grow, wrap"); + + filterList.add(filterPanel); } private void handleAddClicked() { } + public static class FilterPanel { + private JComboBox columnComboBox; + private JComboBox expressionComboBox; + private JTextField valueField; + + public FilterPanel() { + // column + LogsTableModel.Columns[] columns = LogsTableModel.Columns.values(); + columnComboBox = new JComboBox<>(columns); + int colIndex = ArrayUtils.indexOf(columns, LogsTableModel.Columns.TAG); + columnComboBox.setSelectedIndex(colIndex); + + LogFilter.Expression[] expressions = LogFilter.Expression.values(); + expressionComboBox = new JComboBox<>(expressions); + int exprIndex = ArrayUtils.indexOf(expressions, LogFilter.Expression.STARTS_WITH); + expressionComboBox.setSelectedIndex(exprIndex); + + valueField = new JTextField(); + } + } + } diff --git a/src/main/java/com/jpage4500/devicemanager/ui/views/HintTextField.java b/src/main/java/com/jpage4500/devicemanager/ui/views/HintTextField.java index 781cd1d..5520052 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/views/HintTextField.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/views/HintTextField.java @@ -55,12 +55,12 @@ public void focusLost(FocusEvent e) { new DocumentListener() { @Override public void insertUpdate(DocumentEvent documentEvent) { - listener.textChanged(getCleanText()); + if (listener != null) listener.textChanged(getCleanText()); } @Override public void removeUpdate(DocumentEvent documentEvent) { - listener.textChanged(getCleanText()); + if (listener != null) listener.textChanged(getCleanText()); } @Override diff --git a/src/main/java/com/jpage4500/devicemanager/utils/ArrayUtils.java b/src/main/java/com/jpage4500/devicemanager/utils/ArrayUtils.java new file mode 100644 index 0000000..6215112 --- /dev/null +++ b/src/main/java/com/jpage4500/devicemanager/utils/ArrayUtils.java @@ -0,0 +1,35 @@ +package com.jpage4500.devicemanager.utils; + +public class ArrayUtils { + + public static int indexOf(int[] array, int value) { + for (int i = 0; i < array.length; i++) { + if (array[i] == value) return i; + } + return -1; + } + + public static int indexOf(long[] array, long value) { + for (int i = 0; i < array.length; i++) { + if (array[i] == value) return i; + } + return -1; + } + + public static int indexOf(T[] array, T value) { + for (int i = 0; i < array.length; i++) { + T obj = array[i]; + if (obj != null && (obj == value || obj.equals(value))) return i; + } + return -1; + } + + public static int indexOf(String[] valueArr, String searchFor) { + if (valueArr == null) return -1; + for (int i = 0; i < valueArr.length; i++) { + String value = valueArr[i]; + if (TextUtils.equals(value, searchFor)) return i; + } + return -1; + } +} From 5da6e00deca6f28070c56928a132d57824b3754c Mon Sep 17 00:00:00 2001 From: Joe Page Date: Wed, 19 Jun 2024 18:28:44 -0400 Subject: [PATCH 07/11] - working on cleanup tasks --- .../devicemanager/manager/DeviceManager.java | 7 ++- .../devicemanager/ui/DeviceScreen.java | 42 ++++++++++---- .../devicemanager/ui/ExploreScreen.java | 7 ++- .../devicemanager/ui/LogsScreen.java | 6 +- .../ui/dialog/SettingsDialog.java | 4 +- .../devicemanager/ui/views/CustomFrame.java | 57 ++++++++++++------- .../devicemanager/ui/views/CustomTable.java | 11 ++-- 7 files changed, 88 insertions(+), 46 deletions(-) diff --git a/src/main/java/com/jpage4500/devicemanager/manager/DeviceManager.java b/src/main/java/com/jpage4500/devicemanager/manager/DeviceManager.java index 2069c0d..458d688 100644 --- a/src/main/java/com/jpage4500/devicemanager/manager/DeviceManager.java +++ b/src/main/java/com/jpage4500/devicemanager/manager/DeviceManager.java @@ -62,9 +62,10 @@ public class DeviceManager { private final ExecutorService commandExecutorService; private final ScheduledExecutorService scheduledExecutorService; - private final AtomicBoolean isLogging = new AtomicBoolean(false); private ScheduledFuture deviceRefreshRuture; + private final AtomicBoolean isLogging = new AtomicBoolean(false); + private JadbConnection connection; public static DeviceManager getInstance() { @@ -913,6 +914,10 @@ public void handleExit() { process.destroy(); } } + + if (deviceRefreshRuture != null) deviceRefreshRuture.cancel(true); + commandExecutorService.shutdownNow(); + scheduledExecutorService.shutdownNow(); } /** diff --git a/src/main/java/com/jpage4500/devicemanager/ui/DeviceScreen.java b/src/main/java/com/jpage4500/devicemanager/ui/DeviceScreen.java index 8f6a105..d9ff213 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/DeviceScreen.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/DeviceScreen.java @@ -26,6 +26,9 @@ import java.awt.*; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.StringSelection; +import java.awt.desktop.QuitEvent; +import java.awt.desktop.QuitHandler; +import java.awt.desktop.QuitResponse; import java.awt.dnd.DropTarget; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; @@ -146,11 +149,25 @@ protected void initalizeUi() { table.requestFocus(); - Runtime.getRuntime().addShutdownHook(new Thread(this::handleAppExit)); + if (Desktop.isDesktopSupported()) { + Desktop desktop = Desktop.getDesktop(); + desktop.setQuitHandler((quitEvent, quitResponse) -> { + handleAppExit(); + quitResponse.performQuit(); + }); + } else { + Runtime.getRuntime().addShutdownHook(new Thread(this::handleAppExit)); + } } private void handleAppExit() { - table.persist(); + saveFrameSize(); + table.saveTable(); + + // save positions/sizes of any other open windows (only save position of + if (!exploreViewMap.isEmpty()) (exploreViewMap.values().iterator().next()).onWindowStateChanged(WindowState.CLOSED); + if (!logsViewMap.isEmpty()) (logsViewMap.values().iterator().next()).onWindowStateChanged(WindowState.CLOSED); + if (!inputViewMap.isEmpty()) (inputViewMap.values().iterator().next()).onWindowStateChanged(WindowState.CLOSED); DeviceManager.getInstance().handleExit(); } @@ -237,7 +254,7 @@ private void setupTable() { columnModel.getColumn(DeviceTableModel.Columns.FREE.ordinal()).setMaxWidth(80); // restore user-defined column sizes - table.restore(); + table.restoreTable(); sorter = new DeviceRowSorter(model); table.setRowSorter(sorter); @@ -373,7 +390,7 @@ private void setupSystemTray() { // get the SystemTray instance SystemTray tray = SystemTray.getSystemTray(); - BufferedImage image = UiUtils.getImage("android.png", 40, 40); + BufferedImage image = UiUtils.getImage("logo.png", 40, 40); PopupMenu popup = new PopupMenu(); MenuItem openItem = new MenuItem("Open"); openItem.addActionListener(e2 -> bringWindowToFront()); @@ -468,9 +485,9 @@ private void handleHideColumn(int column) { List hiddenColList = SettingsDialog.getHiddenColumnList(); hiddenColList.add(columnType.name()); PreferenceUtils.setPreference(PreferenceUtils.Pref.PREF_HIDDEN_COLUMNS, GsonHelper.toJson(hiddenColList)); - table.persist(); + table.saveTable(); model.setHiddenColumns(hiddenColList); - table.restore(); + table.restoreTable(); } private void handleCopyClipboardFieldCommand() { @@ -634,13 +651,14 @@ private void handleSetProperty(int number) { } private void handleInputCommand() { - Device device = getFirstSelectedDevice(); - if (device == null) return; + Device selectedDevice = getFirstSelectedDevice(); + if (selectedDevice == null) return; - InputScreen inputScreen = inputViewMap.get(device.serial); + InputScreen inputScreen = inputViewMap.get(selectedDevice.serial); if (inputScreen == null) { - inputScreen = new InputScreen(this, device); - inputViewMap.put(device.serial, inputScreen); + if (!selectedDevice.isOnline) return; + inputScreen = new InputScreen(this, selectedDevice); + inputViewMap.put(selectedDevice.serial, inputScreen); } inputScreen.show(); } @@ -928,6 +946,7 @@ public void handleBrowseCommand() { ExploreScreen exploreScreen = exploreViewMap.get(selectedDevice.serial); if (exploreScreen == null) { + if (!selectedDevice.isOnline) return; exploreScreen = new ExploreScreen(this, selectedDevice); exploreViewMap.put(selectedDevice.serial, exploreScreen); } @@ -952,6 +971,7 @@ public void handleLogsCommand() { LogsScreen logsScreen = logsViewMap.get(selectedDevice.serial); if (logsScreen == null) { + if (!selectedDevice.isOnline) return; logsScreen = new LogsScreen(this, selectedDevice); logsViewMap.put(selectedDevice.serial, logsScreen); } diff --git a/src/main/java/com/jpage4500/devicemanager/ui/ExploreScreen.java b/src/main/java/com/jpage4500/devicemanager/ui/ExploreScreen.java index d24267d..1fd6a66 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/ExploreScreen.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/ExploreScreen.java @@ -122,7 +122,8 @@ private void setupStatusBar() { protected void onWindowStateChanged(WindowState state) { super.onWindowStateChanged(state); if (state == WindowState.CLOSED) { - table.persist(); + saveFrameSize(); + table.saveTable(); } } @@ -157,7 +158,7 @@ private void setupMenuBar() { private void closeWindow() { log.trace("closeWindow: {}", device.getDisplayName()); - table.persist(); + table.saveTable(); deviceScreen.handleBrowseClosed(device.serial); dispose(); } @@ -177,7 +178,7 @@ private void setupTable() { TableColumnModel columnModel = table.getColumnModel(); columnModel.getColumn(ExploreTableModel.Columns.SIZE.ordinal()).setPreferredWidth(80); // restore user-defined column sizes - table.restore(); + table.restoreTable(); // ENTER -> click on file KeyStroke enter = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0); diff --git a/src/main/java/com/jpage4500/devicemanager/ui/LogsScreen.java b/src/main/java/com/jpage4500/devicemanager/ui/LogsScreen.java index c08fb0c..dddac65 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/LogsScreen.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/LogsScreen.java @@ -22,7 +22,6 @@ import javax.swing.*; import javax.swing.border.EmptyBorder; -import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumnModel; import java.awt.*; import java.awt.datatransfer.Clipboard; @@ -155,7 +154,8 @@ protected void onWindowStateChanged(WindowState state) { case CLOSED -> { // stop logging when window is closed stopLogging(); - table.persist(); + saveFrameSize(); + table.saveTable(); } case ACTIVATED -> { // start logging if user didn't stop @@ -291,7 +291,7 @@ private void setupTable() { columnModel.getColumn(LogsTableModel.Columns.MSG.ordinal()).setPreferredWidth(700); // restore user-defined column sizes - table.restore(); + table.restoreTable(); // ENTER -> view message KeyStroke enter = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0); diff --git a/src/main/java/com/jpage4500/devicemanager/ui/dialog/SettingsDialog.java b/src/main/java/com/jpage4500/devicemanager/ui/dialog/SettingsDialog.java index 74ab22a..708ecad 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/dialog/SettingsDialog.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/dialog/SettingsDialog.java @@ -159,9 +159,9 @@ private void showColumns() { List selectedItems = checkBoxList.getUnSelectedItems(); log.debug("HIDDEN: {}", GsonHelper.toJson(selectedItems)); PreferenceUtils.setPreference(PreferenceUtils.Pref.PREF_HIDDEN_COLUMNS, GsonHelper.toJson(selectedItems)); - deviceScreen.table.persist(); + deviceScreen.table.saveTable(); deviceScreen.model.setHiddenColumns(selectedItems); - deviceScreen.table.restore(); + deviceScreen.table.restoreTable(); } private void showAppsSettings() { diff --git a/src/main/java/com/jpage4500/devicemanager/ui/views/CustomFrame.java b/src/main/java/com/jpage4500/devicemanager/ui/views/CustomFrame.java index 045f1c4..ef9ada3 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/views/CustomFrame.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/views/CustomFrame.java @@ -5,8 +5,6 @@ import javax.swing.*; import java.awt.*; -import java.awt.event.ComponentAdapter; -import java.awt.event.ComponentEvent; import java.util.prefs.Preferences; /** @@ -33,37 +31,56 @@ public CustomFrame(String prefKey) throws HeadlessException { } private void init() { - restoreFrame(); + restoreFrameSize(); - addComponentListener(new ComponentAdapter() { - @Override - public void componentResized(ComponentEvent e) { - saveFrameSize(); - } - - @Override - public void componentMoved(ComponentEvent e) { - saveFrameSize(); - } - }); + //addComponentListener(new ComponentAdapter() { + // @Override + // public void componentResized(ComponentEvent e) { + // //saveFrameSize(); + // //log.trace("componentResized: w:{}, h:{}", getWidth(), getHeight()); + // } + // + // @Override + // public void componentMoved(ComponentEvent e) { + // //saveFrameSize(); + // //log.trace("componentMoved: x:{}, y:{}", getX(), getY()); + // } + //}); } + /** + * save current frame size + */ protected void saveFrameSize() { Preferences prefs = Preferences.userRoot(); prefs.putInt(prefKey + "-" + FRAME_X, getX()); prefs.putInt(prefKey + "-" + FRAME_Y, getY()); prefs.putInt(prefKey + "-" + FRAME_W, getWidth()); prefs.putInt(prefKey + "-" + FRAME_H, getHeight()); + //log.trace("saveFrameSize: {}: x:{}, y:{}, w:{}, h:{}", prefKey, getX(), getY(), getWidth(), getHeight()); } - private void restoreFrame() { + /** + * restore frame size + */ + private void restoreFrameSize() { Preferences prefs = Preferences.userRoot(); - int x = prefs.getInt(prefKey + "-" + FRAME_X, 200); - int y = prefs.getInt(prefKey + "-" + FRAME_Y, 200); - int w = prefs.getInt(prefKey + "-" + FRAME_W, 500); - int h = prefs.getInt(prefKey + "-" + FRAME_H, 300); + int x = prefs.getInt(prefKey + "-" + FRAME_X, -1); + int y = prefs.getInt(prefKey + "-" + FRAME_Y, -1); + int w = prefs.getInt(prefKey + "-" + FRAME_W, -1); + int h = prefs.getInt(prefKey + "-" + FRAME_H, -1); + + if (w == -1 || h == -1) { + w = 800; + h = 300; + } + if (x == -1 || y == -1) { + Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + x = (screenSize.width - w) / 2; + y = (screenSize.height - h) / 2; + } - //log.debug("restoreFrame: x:{}, y:{}, w:{}, h:{}", x, y, w, h); + //log.trace("restoreFrame: {}: x:{}, y:{}, w:{}, h:{}", prefKey, x, y, w, h); setLocation(x, y); setSize(w, h); } diff --git a/src/main/java/com/jpage4500/devicemanager/ui/views/CustomTable.java b/src/main/java/com/jpage4500/devicemanager/ui/views/CustomTable.java index 33942f8..2411797 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/views/CustomTable.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/views/CustomTable.java @@ -318,14 +318,13 @@ public static class ColumnDetails { int modelPos; } - public boolean restore() { + public boolean restoreTable() { if (prefKey == null) return false; Preferences prefs = Preferences.userRoot(); String detailsStr = prefs.get(prefKey + "-details", null); if (detailsStr == null) return false; List detailsList = GsonHelper.stringToList(detailsStr, ColumnDetails.class); - for (int i = 0; i < detailsList.size(); i++) { - ColumnDetails details = detailsList.get(i); + for (ColumnDetails details : detailsList) { // lookup column by name TableColumn column = getColumnByName(details.header); if (column == null) continue; @@ -335,8 +334,8 @@ public boolean restore() { int modelIndex = column.getModelIndex(); if (modelIndex != details.userPos) { - log.trace("restore: moving: {}, from:{}, to:{}", details.header, modelIndex, details.userPos); - getColumnModel().moveColumn(modelIndex, details.userPos); + //log.trace("restore: moving: {}, from:{}, to:{}", details.header, modelIndex, details.userPos); + //getColumnModel().moveColumn(modelIndex, details.userPos); } } @@ -365,7 +364,7 @@ public TableColumn getColumnByName(Object header) { return null; } - public void persist() { + public void saveTable() { if (prefKey == null) return; Enumeration columns = getColumnModel().getColumns(); From 8d5b7dbe7cfa5442be0b9b81bcdcb47c59498243 Mon Sep 17 00:00:00 2001 From: Joe Page Date: Sat, 22 Jun 2024 15:25:25 -0400 Subject: [PATCH 08/11] - lots more work on keeping user column order and size.. PITA - change how screen sizes and positions are saved.. make them device specific - more refactoring --- .../table/utils/LogsRowSorter.java | 4 + .../devicemanager/ui/BaseScreen.java | 49 +- .../devicemanager/ui/DeviceScreen.java | 55 +- .../devicemanager/ui/ExploreScreen.java | 2 +- .../devicemanager/ui/InputScreen.java | 2 +- .../devicemanager/ui/LogsScreen.java | 42 +- .../devicemanager/ui/MessageViewScreen.java | 2 +- .../ui/dialog/SettingsDialog.java | 4 +- .../devicemanager/ui/views/CustomFrame.java | 88 --- .../devicemanager/ui/views/CustomTable.java | 89 ++- .../devicemanager/ui/views/JSplitButton.java | 569 ------------------ src/main/resources/images/tray_icon.png | Bin 0 -> 10246 bytes 12 files changed, 165 insertions(+), 741 deletions(-) delete mode 100644 src/main/java/com/jpage4500/devicemanager/ui/views/CustomFrame.java delete mode 100644 src/main/java/com/jpage4500/devicemanager/ui/views/JSplitButton.java create mode 100644 src/main/resources/images/tray_icon.png diff --git a/src/main/java/com/jpage4500/devicemanager/table/utils/LogsRowSorter.java b/src/main/java/com/jpage4500/devicemanager/table/utils/LogsRowSorter.java index a7cc5a6..bea778a 100644 --- a/src/main/java/com/jpage4500/devicemanager/table/utils/LogsRowSorter.java +++ b/src/main/java/com/jpage4500/devicemanager/table/utils/LogsRowSorter.java @@ -33,6 +33,10 @@ public void setFilter(LogFilter... logFilterArr) { logsRowFilter.setFilter(logFilterArr); } + public LogFilter[] getFilter() { + return logsRowFilter.logFilterArr; + } + @Override public RowFilter getRowFilter() { return logsRowFilter; diff --git a/src/main/java/com/jpage4500/devicemanager/ui/BaseScreen.java b/src/main/java/com/jpage4500/devicemanager/ui/BaseScreen.java index 9ec2142..36acd3d 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/BaseScreen.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/BaseScreen.java @@ -1,6 +1,7 @@ package com.jpage4500.devicemanager.ui; -import com.jpage4500.devicemanager.ui.views.CustomFrame; +import com.jpage4500.devicemanager.utils.GsonHelper; +import com.jpage4500.devicemanager.utils.PreferenceUtils; import com.jpage4500.devicemanager.utils.UiUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -8,15 +9,19 @@ import javax.swing.*; import java.awt.*; import java.awt.event.*; +import java.util.prefs.Preferences; /** * create and manage device view */ -public class BaseScreen extends CustomFrame { +public class BaseScreen extends JFrame { private static final Logger log = LoggerFactory.getLogger(BaseScreen.class); - public BaseScreen(String prefKey) { - super(prefKey); + private String prefKey; + + public BaseScreen(String prefKey, int defaultWidth, int defaultHeight) { + this.prefKey = prefKey; + restoreFrameSize(defaultWidth, defaultHeight); setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE); @@ -47,6 +52,16 @@ public void windowClosed(WindowEvent e) { } }); + // TODO: handle window resizing + //if (PreferenceUtils.getPreference(PreferenceUtils.PrefBoolean.PREF_DEBUG_MODE)) { + // addComponentListener(new ComponentAdapter() { + // @Override + // public void componentResized(ComponentEvent componentEvent) { + // log.trace("componentResized: {}: W:{}, H:{}", prefKey, getWidth(), getHeight()); + // } + // }); + //} + // NOTE: this breaks dragging the scrollbar on Mac // getRootPane().putClientProperty("apple.awt.draggableWindowBackground", true); } @@ -140,4 +155,30 @@ public void actionPerformed(ActionEvent e) { return action; } + /** + * save current frame size + */ + protected void saveFrameSize() { + Preferences prefs = Preferences.userRoot(); + Rectangle rect = getBounds(); + prefs.put(prefKey, GsonHelper.toJson(rect)); + } + + /** + * restore frame size + */ + private void restoreFrameSize(int defaultWidth, int defaultHeight) { + Preferences prefs = Preferences.userRoot(); + String savedFrameSize = prefs.get(prefKey, null); + Rectangle r = GsonHelper.fromJson(savedFrameSize, Rectangle.class); + if (r == null) { + Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + int x = (screenSize.width - defaultWidth) / 2; + int y = (screenSize.height - defaultHeight) / 2; + r = new Rectangle(x, y, defaultWidth, defaultHeight); + } + setLocation(r.x, r.y); + setSize(r.width, r.height); + } + } diff --git a/src/main/java/com/jpage4500/devicemanager/ui/DeviceScreen.java b/src/main/java/com/jpage4500/devicemanager/ui/DeviceScreen.java index d9ff213..1e7b41c 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/DeviceScreen.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/DeviceScreen.java @@ -22,13 +22,9 @@ import org.slf4j.LoggerFactory; import javax.swing.*; -import javax.swing.table.TableColumnModel; import java.awt.*; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.StringSelection; -import java.awt.desktop.QuitEvent; -import java.awt.desktop.QuitHandler; -import java.awt.desktop.QuitResponse; import java.awt.dnd.DropTarget; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; @@ -62,7 +58,7 @@ public class DeviceScreen extends BaseScreen implements DeviceManager.DeviceList private final Map inputViewMap = new HashMap<>(); public DeviceScreen() { - super("main"); + super("main", 900, 300); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); initalizeUi(); @@ -160,6 +156,13 @@ protected void initalizeUi() { } } + private void exitApp() { + handleAppExit(); + setVisible(false); + dispose(); + System.exit(0); + } + private void handleAppExit() { saveFrameSize(); table.saveTable(); @@ -177,9 +180,7 @@ private void setupMenuBar() { // [CMD + W] = close window createCmdAction(windowMenu, "Close Window", KeyEvent.VK_W, e -> { - setVisible(false); - dispose(); - System.exit(0); + exitApp(); }); // [CMD + 2] = show explorer @@ -242,19 +243,20 @@ private void setupTable() { table.setDefaultRenderer(Device.class, new DeviceCellRenderer()); table.setEmptyText("No Android Devices!"); - // use some default column sizes - TableColumnModel columnModel = table.getColumnModel(); - columnModel.getColumn(DeviceTableModel.Columns.NAME.ordinal()).setPreferredWidth(185); - columnModel.getColumn(DeviceTableModel.Columns.SERIAL.ordinal()).setPreferredWidth(152); - columnModel.getColumn(DeviceTableModel.Columns.PHONE.ordinal()).setPreferredWidth(116); - columnModel.getColumn(DeviceTableModel.Columns.IMEI.ordinal()).setPreferredWidth(147); - columnModel.getColumn(DeviceTableModel.Columns.BATTERY.ordinal()).setPreferredWidth(31); - columnModel.getColumn(DeviceTableModel.Columns.BATTERY.ordinal()).setMaxWidth(31); - columnModel.getColumn(DeviceTableModel.Columns.FREE.ordinal()).setPreferredWidth(66); - columnModel.getColumn(DeviceTableModel.Columns.FREE.ordinal()).setMaxWidth(80); - // restore user-defined column sizes - table.restoreTable(); + if (!table.restoreTable()) { + // use some default column sizes + table.setPreferredColWidth(DeviceTableModel.Columns.NAME.name(), 185); + table.setPreferredColWidth(DeviceTableModel.Columns.SERIAL.name(), 152); + table.setPreferredColWidth(DeviceTableModel.Columns.PHONE.name(), 116); + table.setPreferredColWidth(DeviceTableModel.Columns.IMEI.name(), 147); + table.setPreferredColWidth(DeviceTableModel.Columns.BATTERY.name(), 31); + table.setPreferredColWidth(DeviceTableModel.Columns.FREE.name(), 66); + } + + // set max sizes + table.setMaxColWidth(DeviceTableModel.Columns.BATTERY.name(), 31); + table.setMaxColWidth(DeviceTableModel.Columns.FREE.name(), 80); sorter = new DeviceRowSorter(model); table.setRowSorter(sorter); @@ -390,12 +392,12 @@ private void setupSystemTray() { // get the SystemTray instance SystemTray tray = SystemTray.getSystemTray(); - BufferedImage image = UiUtils.getImage("logo.png", 40, 40); + BufferedImage image = UiUtils.getImage("tray_icon.png", 100, 100); PopupMenu popup = new PopupMenu(); MenuItem openItem = new MenuItem("Open"); openItem.addActionListener(e2 -> bringWindowToFront()); MenuItem quitItem = new MenuItem("Quit"); - quitItem.addActionListener(e2 -> System.exit(0)); + quitItem.addActionListener(e2 -> exitApp()); popup.add(openItem); popup.add(quitItem); trayIcon = new TrayIcon(image, "Android Device Manager", popup); @@ -485,9 +487,18 @@ private void handleHideColumn(int column) { List hiddenColList = SettingsDialog.getHiddenColumnList(); hiddenColList.add(columnType.name()); PreferenceUtils.setPreference(PreferenceUtils.Pref.PREF_HIDDEN_COLUMNS, GsonHelper.toJson(hiddenColList)); + restoreTable(); + } + + public void restoreTable() { table.saveTable(); + List hiddenColList = SettingsDialog.getHiddenColumnList(); model.setHiddenColumns(hiddenColList); table.restoreTable(); + // set max sizes + table.setMaxColWidth(DeviceTableModel.Columns.BATTERY.name(), 31); + table.setMaxColWidth(DeviceTableModel.Columns.FREE.name(), 80); + } private void handleCopyClipboardFieldCommand() { diff --git a/src/main/java/com/jpage4500/devicemanager/ui/ExploreScreen.java b/src/main/java/com/jpage4500/devicemanager/ui/ExploreScreen.java index 1fd6a66..c996fa0 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/ExploreScreen.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/ExploreScreen.java @@ -60,7 +60,7 @@ public class ExploreScreen extends BaseScreen { private boolean useRoot; public ExploreScreen(DeviceScreen deviceScreen, Device device) { - super("browse"); + super("browse-" + device.serial, 500, 500); this.deviceScreen = deviceScreen; this.device = device; setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); diff --git a/src/main/java/com/jpage4500/devicemanager/ui/InputScreen.java b/src/main/java/com/jpage4500/devicemanager/ui/InputScreen.java index d2a8016..9957a24 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/InputScreen.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/InputScreen.java @@ -30,7 +30,7 @@ public class InputScreen extends BaseScreen { private DefaultListModel listModel; public InputScreen(DeviceScreen deviceScreen, Device device) { - super("input"); + super("input-" + device.serial, 300, 300); this.deviceScreen = deviceScreen; this.device = device; setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); diff --git a/src/main/java/com/jpage4500/devicemanager/ui/LogsScreen.java b/src/main/java/com/jpage4500/devicemanager/ui/LogsScreen.java index dddac65..566a09e 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/LogsScreen.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/LogsScreen.java @@ -22,7 +22,6 @@ import javax.swing.*; import javax.swing.border.EmptyBorder; -import javax.swing.table.TableColumnModel; import java.awt.*; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.StringSelection; @@ -64,7 +63,7 @@ public class LogsScreen extends BaseScreen implements DeviceManager.DeviceLogLis public boolean isLoggedPaused; // true when user clicks on 'stop logging' public LogsScreen(DeviceScreen deviceScreen, Device device) { - super("logs"); + super("logs-" + device.serial, 1100, 800); this.deviceScreen = deviceScreen; this.device = device; setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); @@ -278,20 +277,21 @@ private void setupTable() { table.setModel(model); table.setDefaultRenderer(LogEntry.class, new LogsCellRenderer()); - // default column sizes - TableColumnModel columnModel = table.getColumnModel(); - columnModel.getColumn(LogsTableModel.Columns.LEVEL.ordinal()).setPreferredWidth(28); - columnModel.getColumn(LogsTableModel.Columns.LEVEL.ordinal()).setMaxWidth(35); - columnModel.getColumn(LogsTableModel.Columns.PID.ordinal()).setPreferredWidth(60); - columnModel.getColumn(LogsTableModel.Columns.PID.ordinal()).setMaxWidth(100); - columnModel.getColumn(LogsTableModel.Columns.TID.ordinal()).setPreferredWidth(60); - columnModel.getColumn(LogsTableModel.Columns.TID.ordinal()).setMaxWidth(100); - columnModel.getColumn(LogsTableModel.Columns.DATE.ordinal()).setPreferredWidth(159); - columnModel.getColumn(LogsTableModel.Columns.APP.ordinal()).setPreferredWidth(150); - columnModel.getColumn(LogsTableModel.Columns.MSG.ordinal()).setPreferredWidth(700); - // restore user-defined column sizes - table.restoreTable(); + if (!table.restoreTable()) { + // use some default column sizes + table.setPreferredColWidth(LogsTableModel.Columns.LEVEL.toString(), 28); + table.setPreferredColWidth(LogsTableModel.Columns.PID.toString(), 60); + table.setPreferredColWidth(LogsTableModel.Columns.TID.toString(), 60); + table.setPreferredColWidth(LogsTableModel.Columns.DATE.toString(), 159); + table.setPreferredColWidth(LogsTableModel.Columns.APP.toString(), 150); + table.setPreferredColWidth(LogsTableModel.Columns.TAG.toString(), 200); + table.setPreferredColWidth(LogsTableModel.Columns.MSG.toString(), 700); + } + + table.setMaxColWidth(LogsTableModel.Columns.LEVEL.toString(), 35); + table.setMaxColWidth(LogsTableModel.Columns.PID.toString(), 100); + table.setMaxColWidth(LogsTableModel.Columns.TID.toString(), 100); // ENTER -> view message KeyStroke enter = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0); @@ -522,12 +522,16 @@ private void scrollToFollow() { } private void refreshUi() { - // viewing X int rowCount = table.getRowCount(); - int totalRows = model.getRowCount(); String msg = "viewing " + rowCount; - if (totalRows > 0 && totalRows > rowCount) { - msg += " / " + totalRows; + + LogFilter[] filter = sorter.getFilter(); + if (filter != null) { + int totalRows = model.getRowCount(); + // viewing X / Y + if (totalRows > 0 && totalRows > rowCount) { + msg += " / " + totalRows; + } } statusBar.setLeftLabel(msg); } diff --git a/src/main/java/com/jpage4500/devicemanager/ui/MessageViewScreen.java b/src/main/java/com/jpage4500/devicemanager/ui/MessageViewScreen.java index b143246..714e183 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/MessageViewScreen.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/MessageViewScreen.java @@ -41,7 +41,7 @@ public class MessageViewScreen extends BaseScreen { private JButton editButton; public MessageViewScreen(DeviceScreen deviceScreen) { - super("message"); + super("message", 500, 500); this.deviceScreen = deviceScreen; setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE); initalizeUi(); diff --git a/src/main/java/com/jpage4500/devicemanager/ui/dialog/SettingsDialog.java b/src/main/java/com/jpage4500/devicemanager/ui/dialog/SettingsDialog.java index 708ecad..3e400af 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/dialog/SettingsDialog.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/dialog/SettingsDialog.java @@ -159,9 +159,7 @@ private void showColumns() { List selectedItems = checkBoxList.getUnSelectedItems(); log.debug("HIDDEN: {}", GsonHelper.toJson(selectedItems)); PreferenceUtils.setPreference(PreferenceUtils.Pref.PREF_HIDDEN_COLUMNS, GsonHelper.toJson(selectedItems)); - deviceScreen.table.saveTable(); - deviceScreen.model.setHiddenColumns(selectedItems); - deviceScreen.table.restoreTable(); + deviceScreen.restoreTable(); } private void showAppsSettings() { diff --git a/src/main/java/com/jpage4500/devicemanager/ui/views/CustomFrame.java b/src/main/java/com/jpage4500/devicemanager/ui/views/CustomFrame.java deleted file mode 100644 index ef9ada3..0000000 --- a/src/main/java/com/jpage4500/devicemanager/ui/views/CustomFrame.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.jpage4500.devicemanager.ui.views; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.swing.*; -import java.awt.*; -import java.util.prefs.Preferences; - -/** - * - */ -public class CustomFrame extends JFrame { - private static final Logger log = LoggerFactory.getLogger(CustomFrame.class); - - private static final String FRAME_X = "frame-x"; - private static final String FRAME_Y = "frame-y"; - private static final String FRAME_W = "frame-w"; - private static final String FRAME_H = "frame-h"; - - protected String prefKey; - - public CustomFrame() throws HeadlessException { - init(); - } - - public CustomFrame(String prefKey) throws HeadlessException { - super(""); - this.prefKey = prefKey; - init(); - } - - private void init() { - restoreFrameSize(); - - //addComponentListener(new ComponentAdapter() { - // @Override - // public void componentResized(ComponentEvent e) { - // //saveFrameSize(); - // //log.trace("componentResized: w:{}, h:{}", getWidth(), getHeight()); - // } - // - // @Override - // public void componentMoved(ComponentEvent e) { - // //saveFrameSize(); - // //log.trace("componentMoved: x:{}, y:{}", getX(), getY()); - // } - //}); - } - - /** - * save current frame size - */ - protected void saveFrameSize() { - Preferences prefs = Preferences.userRoot(); - prefs.putInt(prefKey + "-" + FRAME_X, getX()); - prefs.putInt(prefKey + "-" + FRAME_Y, getY()); - prefs.putInt(prefKey + "-" + FRAME_W, getWidth()); - prefs.putInt(prefKey + "-" + FRAME_H, getHeight()); - //log.trace("saveFrameSize: {}: x:{}, y:{}, w:{}, h:{}", prefKey, getX(), getY(), getWidth(), getHeight()); - } - - /** - * restore frame size - */ - private void restoreFrameSize() { - Preferences prefs = Preferences.userRoot(); - int x = prefs.getInt(prefKey + "-" + FRAME_X, -1); - int y = prefs.getInt(prefKey + "-" + FRAME_Y, -1); - int w = prefs.getInt(prefKey + "-" + FRAME_W, -1); - int h = prefs.getInt(prefKey + "-" + FRAME_H, -1); - - if (w == -1 || h == -1) { - w = 800; - h = 300; - } - if (x == -1 || y == -1) { - Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); - x = (screenSize.width - w) / 2; - y = (screenSize.height - h) / 2; - } - - //log.trace("restoreFrame: {}: x:{}, y:{}, w:{}, h:{}", prefKey, x, y, w, h); - setLocation(x, y); - setSize(w, h); - } - -} diff --git a/src/main/java/com/jpage4500/devicemanager/ui/views/CustomTable.java b/src/main/java/com/jpage4500/devicemanager/ui/views/CustomTable.java index 2411797..8e5f3ea 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/views/CustomTable.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/views/CustomTable.java @@ -1,16 +1,11 @@ package com.jpage4500.devicemanager.ui.views; -import com.jpage4500.devicemanager.utils.GsonHelper; -import com.jpage4500.devicemanager.utils.PreferenceUtils; -import com.jpage4500.devicemanager.utils.UiUtils; +import com.jpage4500.devicemanager.utils.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.swing.*; -import javax.swing.table.JTableHeader; -import javax.swing.table.TableCellRenderer; -import javax.swing.table.TableColumn; -import javax.swing.table.TableModel; +import javax.swing.table.*; import java.awt.*; import java.awt.dnd.DropTarget; import java.awt.dnd.DropTargetDragEvent; @@ -311,11 +306,13 @@ private void scrollPage(boolean isUp) { } public static class ColumnDetails { - Object header; + String name; int width; - int maxWidth; int userPos; int modelPos; + + @ExcludeFromSerialization + TableColumn column; } public boolean restoreTable() { @@ -324,64 +321,90 @@ public boolean restoreTable() { String detailsStr = prefs.get(prefKey + "-details", null); if (detailsStr == null) return false; List detailsList = GsonHelper.stringToList(detailsStr, ColumnDetails.class); + + // TODO: this is messy but it's the most reliable way I've found to retain user column order.. + TableColumnModel columnModel = getColumnModel(); + // 1) backup columns to ColumnDetails for (ColumnDetails details : detailsList) { - // lookup column by name - TableColumn column = getColumnByName(details.header); - if (column == null) continue; - //log.trace("restore: {}: w:{}, max:{}", details.header, details.width, details.maxWidth); - column.setPreferredWidth(details.width); - if (details.maxWidth > 0) column.setMaxWidth(details.maxWidth); - - int modelIndex = column.getModelIndex(); - if (modelIndex != details.userPos) { - //log.trace("restore: moving: {}, from:{}, to:{}", details.header, modelIndex, details.userPos); - //getColumnModel().moveColumn(modelIndex, details.userPos); + details.column = getColumnByName(details.name); + if (details.column != null) { + columnModel.removeColumn(details.column); } } -// TableColumnModel columnModel = getColumnModel(); -// for (ColumnDetails details : detailsList) { -// if (details.modelPos != details.userPos) { -// log.trace("restore: move:{} to:{}", details.modelPos, details.userPos); -// columnModel.moveColumn(details.modelPos, details.userPos); -// } -// } + // 2) backup any additional columns (if any) + List additionalColumnList = new ArrayList<>(); + for (int i = 0; i < columnModel.getColumnCount(); i++) { + TableColumn column = columnModel.getColumn(i); + additionalColumnList.add(column); + } + + // 3) remove additional columns (if any) + for (TableColumn column : additionalColumnList) { + columnModel.removeColumn(column); + } + + // 4) re-add columns in order they were saved + for (ColumnDetails details : detailsList) { + if (details.column != null) { + columnModel.addColumn(details.column); + details.column.setPreferredWidth(details.width); + } + } + + // 5) re-add additional columns + for (TableColumn column : additionalColumnList) { + columnModel.addColumn(column); + } return true; } /** * get column by header name (NOTE: will return null and not throw an Exception when not found) */ - public TableColumn getColumnByName(Object header) { + public TableColumn getColumnByName(String searchName) { Enumeration columns = getColumnModel().getColumns(); Iterator iterator = columns.asIterator(); while (iterator.hasNext()) { TableColumn column = iterator.next(); - if (column.getHeaderValue().equals(header)) { + String columnName = column.getHeaderValue().toString(); + if (TextUtils.equalsIgnoreCase(columnName, searchName)) { return column; } } + log.error("getColumnByName: NOT_FOUND:{}", searchName); return null; } + public void setPreferredColWidth(String colName, int preferredWidth) { + TableColumn column = getColumnByName(colName); + if (column == null) return; + column.setPreferredWidth(preferredWidth); + } + + public void setMaxColWidth(String colName, int maxWidth) { + TableColumn column = getColumnByName(colName); + if (column == null) return; + column.setMaxWidth(maxWidth); + } + public void saveTable() { if (prefKey == null) return; + // save columns in display order Enumeration columns = getColumnModel().getColumns(); Iterator iter = columns.asIterator(); List detailList = new ArrayList<>(); for (int i = 0; iter.hasNext(); i++) { TableColumn column = iter.next(); ColumnDetails details = new ColumnDetails(); - details.header = column.getHeaderValue(); + details.name = column.getHeaderValue().toString(); details.userPos = i; details.modelPos = column.getModelIndex(); details.width = column.getWidth(); int maxWidth = column.getMaxWidth(); - // only need to set maxWidth if one is defined (and it won't typicically be very large if it is) - if (maxWidth < 500) details.maxWidth = maxWidth; detailList.add(details); - //log.trace("persist: {}", GsonHelper.toJson(details)); + //log.trace("persist: {}, pos:{}, i:{}, w:{}, max:{}", details.header, i, details.modelPos, details.width, details.maxWidth); } Preferences prefs = Preferences.userRoot(); diff --git a/src/main/java/com/jpage4500/devicemanager/ui/views/JSplitButton.java b/src/main/java/com/jpage4500/devicemanager/ui/views/JSplitButton.java deleted file mode 100644 index b03faa5..0000000 --- a/src/main/java/com/jpage4500/devicemanager/ui/views/JSplitButton.java +++ /dev/null @@ -1,569 +0,0 @@ -/* - * Copyright (C) 2016, 2018 Randall Wood - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.jpage4500.devicemanager.ui.views; - -//import com.alexandriasoftware.jsplitbutton.action.ButtonClickedActionListener; -//import com.alexandriasoftware.jsplitbutton.action.SplitButtonClickedActionListener; - -import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; -import java.awt.event.MouseMotionListener; -import java.awt.image.BufferedImage; -import javax.swing.Icon; -import javax.swing.JButton; -import javax.swing.JPopupMenu; -import javax.swing.UIManager; - -/** - * An implementation of a "split" button. The left side acts like a normal - * button, right side has a jPopupMenu attached. If there is no attached menu, - * the right side of the split button appears disabled, and clicking anywhere in - * the button triggers the normal button action. - *

- * Implement {@link ButtonClickedActionListener} to handle the event raised when - * the main button is clicked and {@link SplitButtonClickedActionListener} to - * handle the event raised when the popup menu is triggered. - * - * @author Naveed Quadri 2012 - * @author Randall Wood 2016 - */ -public class JSplitButton extends JButton { - - public interface SplitButtonClickedActionListener extends ActionListener { - - } - - public interface ButtonClickedActionListener extends ActionListener { - - } - - /** - * Key used for serialization. - */ - private static final long serialVersionUID = 1L; - /** - * Vertical spacing around visual separator. - */ - private int separatorSpacing = 4; - /** - * Width of split button containing menu arrow. - */ - private int splitWidth = 22; - /** - * Size of menu arrow. - */ - private int arrowSize = 8; - /** - * True if mouse is hovering over split; false otherwise. - */ - private boolean onSplit = false; - /** - * Component in split button. - */ - private Rectangle splitRectangle = new Rectangle(); - /** - * Menu with split button. - */ - private JPopupMenu popupMenu; - /** - * True if menu should always be displayed when button clicked; false if - * menu should only be displayed when split clicked. - */ - private boolean alwaysPopup; - /** - * Color of menu arrow. - */ - private Color arrowColor = Color.BLACK; - /** - * Color or menu arrow when disabled. - */ - private Color disabledArrowColor = Color.GRAY; - private transient Image image; - private transient Image disabledImage; - private final transient Listener listener; - - /** - * Creates a button with initial text and an icon. - * - * @param text the text of the button - * @param icon the Icon image to display on the button - */ - public JSplitButton(final String text, final Icon icon) { - super(text, icon); - this.listener = new Listener(); - super.addMouseMotionListener(this.listener); - super.addMouseListener(this.listener); - super.addActionListener(this.listener); - } - - /** - * Creates a button with text. - * - * @param text the text of the button - */ - public JSplitButton(final String text) { - this(text, null); - } - - /** - * Creates a button with an icon. - * - * @param icon the Icon image to display on the button - */ - public JSplitButton(final Icon icon) { - this(null, icon); - } - - /** - * Creates a button with no set text or icon. - */ - public JSplitButton() { - this(null, null); - } - - /** - * Returns the JPopupMenu if set, null otherwise. - * - * @return JPopupMenu - */ - public JPopupMenu getPopupMenu() { - return popupMenu; - } - - /** - * Sets the JPopupMenu to be displayed, when the split part of the button is - * clicked. - * - * @param popupMenu the menu to display - */ - public void setPopupMenu(final JPopupMenu popupMenu) { - this.popupMenu = popupMenu; - image = null; //to repaint the arrow image - } - - /** - * Returns the separatorSpacing. Separator spacing is the space above and - * below the separator (the line drawn when you hover your mouse over the - * split part of the button). - * - * @return the spacing - */ - public int getSeparatorSpacing() { - return separatorSpacing; - } - - /** - * Sets the separatorSpacing. Separator spacing is the space above and below - * the separator (the line drawn when you hover your mouse over the split - * part of the button). - * - * @param separatorSpacing the spacing - */ - public void setSeparatorSpacing(final int separatorSpacing) { - this.separatorSpacing = separatorSpacing; - } - - /** - * Show the popup menu, if attached, even if the button part is clicked. - * - * @return true if alwaysPopup, false otherwise. - */ - public boolean isAlwaysPopup() { - return alwaysPopup; - } - - /** - * Show the popup menu, if attached, even if the button part is clicked. - * - * @param alwaysPopup true to show the attached JPopupMenu even if the - * button part is clicked, false otherwise - */ - public void setAlwaysPopup(final boolean alwaysPopup) { - this.alwaysPopup = alwaysPopup; - } - - /** - * Gets the color of the arrow. - * - * @return the color of the arrow - */ - public Color getArrowColor() { - return arrowColor; - } - - /** - * Set the arrow color. - * - * @param arrowColor the color of the arrow - */ - public void setArrowColor(final Color arrowColor) { - this.arrowColor = arrowColor; - image = null; // to repaint the image with the new color - } - - /** - * Gets the disabled arrow color. - * - * @return color of the arrow if no popup menu is attached. - */ - public Color getDisabledArrowColor() { - return disabledArrowColor; - } - - /** - * Sets the disabled arrow color. - * - * @param disabledArrowColor color of the arrow if no popup menu is - * attached. - */ - public void setDisabledArrowColor(final Color disabledArrowColor) { - this.disabledArrowColor = disabledArrowColor; - image = null; //to repaint the image with the new color - } - - /** - * Splitwidth is the width of the split part of the button. - * - * @return the width of the split - */ - public int getSplitWidth() { - return splitWidth; - } - - /** - * Splitwidth is the width of the split part of the button. - * - * @param splitWidth the width of the split - */ - public void setSplitWidth(final int splitWidth) { - this.splitWidth = splitWidth; - } - - /** - * Gets the size of the arrow. - * - * @return size of the arrow - */ - public int getArrowSize() { - return arrowSize; - } - - /** - * Sets the size of the arrow. - * - * @param arrowSize the size of the arrow - */ - public void setArrowSize(final int arrowSize) { - this.arrowSize = arrowSize; - image = null; //to repaint the image with the new size - } - - /** - * Gets the image to be drawn in the split part. If no is set, a new image - * is created with the triangle. - * - * @return image - */ - public Image getImage() { - if (image != null) { - return image; - } else if (popupMenu == null) { - return this.getDisabledImage(); - } else { - image = this.getImage(this.arrowSize, this.arrowColor); - return image; - } - } - - /** - * Sets the image to draw instead of the triangle. - * - * @param image the image - */ - public void setImage(final Image image) { - this.image = image; - } - - /** - * Gets the disabled image to be drawn in the split part. If no is set, a - * new image is created with the triangle. - * - * @return image - */ - public Image getDisabledImage() { - if (disabledImage != null) { - return disabledImage; - } else { - disabledImage = this.getImage(this.arrowSize, this.disabledArrowColor); - return disabledImage; - } - } - - /** - * Draws the default arrow image in the specified color. - * - * @param color - * @return image - */ - private Image getImage(final int size, final Color color) { - Graphics2D g; - BufferedImage img = new BufferedImage(size, size, BufferedImage.TYPE_INT_RGB); - g = img.createGraphics(); - g.setColor(Color.WHITE); - g.fillRect(0, 0, img.getWidth(), img.getHeight()); - g.setColor(color); - // this creates a triangle facing right > - g.fillPolygon(new int[]{0, 0, size / 2}, new int[]{0, size, size / 2}, 3); - g.dispose(); - // rotate it to face downwards - img = rotate(img, 90); - BufferedImage dimg = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_ARGB); - g = dimg.createGraphics(); - g.setComposite(AlphaComposite.Src); - g.drawImage(img, null, 0, 0); - g.dispose(); - for (int i = 0; i < dimg.getHeight(); i++) { - for (int j = 0; j < dimg.getWidth(); j++) { - if (dimg.getRGB(j, i) == Color.WHITE.getRGB()) { - dimg.setRGB(j, i, 0x8F1C1C); - } - } - } - - return Toolkit.getDefaultToolkit().createImage(dimg.getSource()); - } - - /** - * Sets the disabled image to draw instead of the triangle. - * - * @param image the new image to use - */ - public void setDisabledImage(final Image image) { - this.disabledImage = image; - } - - @Override - public Dimension getPreferredSize() { - Dimension size = super.getPreferredSize(); - if (popupMenu != null) { - size.width = size.width + getSplitWidth(); - } - return size; - } - - @Override - protected void paintComponent(final Graphics g) { - super.paintComponent(g); - Color oldColor = g.getColor(); - splitRectangle = new Rectangle(getWidth() - splitWidth, 0, splitWidth, getHeight()); - g.translate(splitRectangle.x, splitRectangle.y); - int mh = getHeight() / 2; - int mw = splitWidth / 2; - g.drawImage((isEnabled() ? getImage() : getDisabledImage()), mw - arrowSize / 2, mh + 2 - arrowSize / 2, null); - if (onSplit && !alwaysPopup && popupMenu != null) { - g.setColor(UIManager.getLookAndFeelDefaults().getColor("Button.background")); - g.drawLine(1, separatorSpacing + 2, 1, getHeight() - separatorSpacing - 2); - g.setColor(UIManager.getLookAndFeelDefaults().getColor("Button.shadow")); - g.drawLine(2, separatorSpacing + 2, 2, getHeight() - separatorSpacing - 2); - } - g.setColor(oldColor); - g.translate(-splitRectangle.x, -splitRectangle.y); - } - - /** - * Rotates the given image with the specified angle. - * - * @param img image to rotate - * @param angle angle of rotation - * @return rotated image - */ - private BufferedImage rotate(final BufferedImage img, final int angle) { - int w = img.getWidth(); - int h = img.getHeight(); - BufferedImage dimg = new BufferedImage(w, h, img.getType()); - Graphics2D g = dimg.createGraphics(); - g.rotate(Math.toRadians(angle), w / 2.0, h / 2.0); - g.drawImage(img, null, 0, 0); - return dimg; - } - - /** - * Add a {@link ButtonClickedActionListener} to the button. This listener - * will be notified whenever the button part is clicked. - * - * @param l the listener to add. - */ - public void addButtonClickedActionListener(final ButtonClickedActionListener l) { - listenerList.add(ButtonClickedActionListener.class, l); - } - - /** - * Remove a {@link ButtonClickedActionListener} from the button. - * - * @param l the listener to remove. - */ - public void removeButtonClickedActionListener(final ButtonClickedActionListener l) { - listenerList.remove(ButtonClickedActionListener.class, l); - } - - /** - * Add a {@link SplitButtonClickedActionListener} to the button. This - * listener will be notified whenever the split part is clicked. - * - * @param l the listener to add. - */ - public void addSplitButtonClickedActionListener(final SplitButtonClickedActionListener l) { - listenerList.add(SplitButtonClickedActionListener.class, l); - } - - /** - * Remove a {@link SplitButtonClickedActionListener} from the button. - * - * @param l the listener to remove. - */ - public void removeSplitButtonClickedActionListener(final SplitButtonClickedActionListener l) { - listenerList.remove(SplitButtonClickedActionListener.class, l); - } - - /** - * @return the listener - */ - Listener getListener() { - return listener; - } - - /** - * Listener for internal changes within the JSplitButton itself. - *

- * Package private so its available to tests. - */ - class Listener implements MouseMotionListener, MouseListener, ActionListener { - - @Override - public void actionPerformed(final ActionEvent e) { - if (popupMenu == null) { - fireButtonClicked(e); - } else if (alwaysPopup) { - popupMenu.show(JSplitButton.this, getWidth() - (int) popupMenu.getPreferredSize().getWidth(), getHeight()); - fireButtonClicked(e); - } else if (onSplit) { - popupMenu.show(JSplitButton.this, getWidth() - (int) popupMenu.getPreferredSize().getWidth(), getHeight()); - fireSplitButtonClicked(e); - } else { - fireButtonClicked(e); - } - } - - /** - * Notifies all listeners that have registered interest for notification - * on this event type. The event instance is lazily created using the - * {@code event} parameter. - * - * @param event the {@code ActionEvent} object - * @see EventListenerList - */ - private void fireButtonClicked(final ActionEvent event) { - // Guaranteed to return a non-null array - this.fireActionEvent(event, listenerList.getListeners(ButtonClickedActionListener.class)); - } - - /** - * Notifies all listeners that have registered interest for notification - * on this event type. The event instance is lazily created using the - * {@code event} parameter. - * - * @param event the {@code ActionEvent} object - * @see EventListenerList - */ - private void fireSplitButtonClicked(final ActionEvent event) { - // Guaranteed to return a non-null array - this.fireActionEvent(event, listenerList.getListeners(SplitButtonClickedActionListener.class)); - } - - /** - * Notifies all listeners that have registered interest for notification - * on this event type. The event instance is lazily created using the - * {@code event} parameter. - * - * @param event the {@code ActionEvent} object - * @param singleEventListeners the array of event-specific listeners, - * either - * {@link ButtonClickedActionListener}s or - * {@link SplitButtonClickedActionListener}s - * @see EventListenerList - */ - private void fireActionEvent(final ActionEvent event, ActionListener[] singleEventListeners) { - if (singleEventListeners.length != 0) { - String actionCommand = event.getActionCommand(); - if (actionCommand == null) { - actionCommand = getActionCommand(); - } - ActionEvent e = new ActionEvent(JSplitButton.this, - ActionEvent.ACTION_PERFORMED, - actionCommand, - event.getWhen(), - event.getModifiers()); - // Process the listeners last to first - for (int i = singleEventListeners.length - 1; i >= 0; i--) { - singleEventListeners[i].actionPerformed(e); - } - } - } - - @Override - public void mouseExited(final MouseEvent e) { - onSplit = false; - repaint(splitRectangle); - } - - @Override - public void mouseMoved(final MouseEvent e) { - onSplit = splitRectangle.contains(e.getPoint()); - repaint(splitRectangle); - } - - // - @Override - public void mouseDragged(final MouseEvent e) { - // required by MouseMotionListener API, but ignored as drag/drop - // not intrisicly supported within this widget - } - - @Override - public void mouseClicked(final MouseEvent e) { - // required by MouseListener API, but handled by actionPerformed() - } - - @Override - public void mousePressed(final MouseEvent e) { - // required by MouseListener API, but handled by actionPerformed() - } - - @Override - public void mouseReleased(final MouseEvent e) { - // required by MouseListener API, but handled by actionPerformed() - } - - @Override - public void mouseEntered(final MouseEvent e) { - // required by MouseListener API, but handled by mouseMoved() - } - // - } -} diff --git a/src/main/resources/images/tray_icon.png b/src/main/resources/images/tray_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b438eba5fdace753f4e4091617615fe0d36abc71 GIT binary patch literal 10246 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 literal 0 HcmV?d00001 From b898b9936e719d6b5e9ddb34c76bded8ecabbd09 Mon Sep 17 00:00:00 2001 From: Joe Page Date: Sat, 22 Jun 2024 15:42:44 -0400 Subject: [PATCH 09/11] - set max col sizes for explore screen --- .../devicemanager/ui/ExploreScreen.java | 10 +++--- .../devicemanager/ui/views/CustomTable.java | 36 +++++++++++++++---- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/jpage4500/devicemanager/ui/ExploreScreen.java b/src/main/java/com/jpage4500/devicemanager/ui/ExploreScreen.java index c996fa0..bc164f1 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/ExploreScreen.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/ExploreScreen.java @@ -174,11 +174,13 @@ private void setupTable() { //table.getTableHeader().setDefaultRenderer(new TableHeaderRenderer()); table.setEmptyText("No Files!"); - // default column sizes - TableColumnModel columnModel = table.getColumnModel(); - columnModel.getColumn(ExploreTableModel.Columns.SIZE.ordinal()).setPreferredWidth(80); // restore user-defined column sizes - table.restoreTable(); + if (!table.restoreTable()) { + // default column sizes + table.setPreferredColWidth(ExploreTableModel.Columns.SIZE.toString(), 80); + } + table.setMaxColWidth(ExploreTableModel.Columns.SIZE.toString(), 100); + table.setMaxColWidth(ExploreTableModel.Columns.DATE.toString(), 167); // ENTER -> click on file KeyStroke enter = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0); diff --git a/src/main/java/com/jpage4500/devicemanager/ui/views/CustomTable.java b/src/main/java/com/jpage4500/devicemanager/ui/views/CustomTable.java index 8e5f3ea..b8fb1f4 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/views/CustomTable.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/views/CustomTable.java @@ -115,6 +115,33 @@ public void mouseClicked(MouseEvent e) { } } }); + + // TODO: add to log column sizes +// getColumnModel().addColumnModelListener(new TableColumnModelListener() { +// @Override +// public void columnAdded(TableColumnModelEvent tableColumnModelEvent) { +// } +// +// @Override +// public void columnRemoved(TableColumnModelEvent tableColumnModelEvent) { +// } +// +// @Override +// public void columnMoved(TableColumnModelEvent tableColumnModelEvent) { +// } +// +// @Override +// public void columnMarginChanged(ChangeEvent changeEvent) { +// TableColumn resizingColumn = getTableHeader().getResizingColumn(); +// if (resizingColumn != null) { +// log.trace("columnMarginChanged: {}, {}", resizingColumn.getHeaderValue(), resizingColumn.getWidth()); +// } +// } +// +// @Override +// public void columnSelectionChanged(ListSelectionEvent listSelectionEvent) { +// } +// }); } public void setDoubleClickListener(DoubleClickListener doubleClickListener) { @@ -334,13 +361,10 @@ public boolean restoreTable() { // 2) backup any additional columns (if any) List additionalColumnList = new ArrayList<>(); - for (int i = 0; i < columnModel.getColumnCount(); i++) { - TableColumn column = columnModel.getColumn(i); + Iterator iterator = columnModel.getColumns().asIterator(); + while (iterator.hasNext()) { + TableColumn column = iterator.next(); additionalColumnList.add(column); - } - - // 3) remove additional columns (if any) - for (TableColumn column : additionalColumnList) { columnModel.removeColumn(column); } From 208cff46673113a2b87b521e4ebbde05419ab703 Mon Sep 17 00:00:00 2001 From: Joe Page Date: Thu, 18 Jul 2024 10:35:18 -0400 Subject: [PATCH 10/11] - handle case when device name can't be fetched --- .../com/jpage4500/devicemanager/manager/DeviceManager.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/jpage4500/devicemanager/manager/DeviceManager.java b/src/main/java/com/jpage4500/devicemanager/manager/DeviceManager.java index 458d688..77ae08f 100644 --- a/src/main/java/com/jpage4500/devicemanager/manager/DeviceManager.java +++ b/src/main/java/com/jpage4500/devicemanager/manager/DeviceManager.java @@ -241,7 +241,6 @@ private void fetchDeviceDetails(Device device, boolean fullRefresh, DeviceListen device.busyCounter.incrementAndGet(); listener.handleDeviceUpdated(device); if (fullRefresh) { - // -- device nickname -- fetchNickname(device); @@ -379,7 +378,11 @@ private void fetchCustomProperties(Device device) { private void fetchNickname(Device device) { ShellResult result = runShell(device, COMMAND_DEVICE_NICKNAME); if (result.isSuccess && !result.resultList.isEmpty()) { - device.nickname = result.resultList.get(0).trim(); + String nickname = result.resultList.get(0).trim(); + // look for error: "cmd: Can't find service: settings" + if (!TextUtils.containsIgnoreCase(nickname, "Can't find service")) { + device.nickname = nickname; + } } } From 74b887f14a6201b347d5972f6511ba2879db4052 Mon Sep 17 00:00:00 2001 From: Joe Page Date: Thu, 18 Jul 2024 10:36:14 -0400 Subject: [PATCH 11/11] - WIP on filters - change system tray icon --- .../devicemanager/ui/DeviceScreen.java | 2 +- .../ui/dialog/AddFilterDialog.java | 43 ++++++++++++++---- .../devicemanager/ui/views/CustomTable.java | 12 ++++- .../jpage4500/devicemanager/utils/Utils.java | 19 ++++++++ src/main/resources/images/system_tray.png | Bin 0 -> 14529 bytes 5 files changed, 64 insertions(+), 12 deletions(-) create mode 100644 src/main/resources/images/system_tray.png diff --git a/src/main/java/com/jpage4500/devicemanager/ui/DeviceScreen.java b/src/main/java/com/jpage4500/devicemanager/ui/DeviceScreen.java index 1e7b41c..4d1e29b 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/DeviceScreen.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/DeviceScreen.java @@ -392,7 +392,7 @@ private void setupSystemTray() { // get the SystemTray instance SystemTray tray = SystemTray.getSystemTray(); - BufferedImage image = UiUtils.getImage("tray_icon.png", 100, 100); + BufferedImage image = UiUtils.getImage("system_tray.png", 100, 100); PopupMenu popup = new PopupMenu(); MenuItem openItem = new MenuItem("Open"); openItem.addActionListener(e2 -> bringWindowToFront()); diff --git a/src/main/java/com/jpage4500/devicemanager/ui/dialog/AddFilterDialog.java b/src/main/java/com/jpage4500/devicemanager/ui/dialog/AddFilterDialog.java index 6e65caf..1cdfb72 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/dialog/AddFilterDialog.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/dialog/AddFilterDialog.java @@ -4,6 +4,7 @@ import com.jpage4500.devicemanager.table.LogsTableModel; import com.jpage4500.devicemanager.ui.views.HintTextField; import com.jpage4500.devicemanager.utils.ArrayUtils; +import com.jpage4500.devicemanager.utils.UiUtils; import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,7 +19,7 @@ public class AddFilterDialog extends JPanel { private LogFilter logFilter; private HintTextField nameTextField; - private List filterList; + private List panelList; public static LogFilter showAddFilterDialog(Component frame, LogFilter logFilter) { String okButton = logFilter == null ? "Save" : "Update"; @@ -35,7 +36,7 @@ public static LogFilter showAddFilterDialog(Component frame, LogFilter logFilter public AddFilterDialog(LogFilter logFilter) { if (logFilter == null) logFilter = new LogFilter(); this.logFilter = logFilter; - filterList = new ArrayList<>(); + panelList = new ArrayList<>(); initalizeUi(); } @@ -44,14 +45,16 @@ protected void initalizeUi() { setLayout(new MigLayout("fillx", "[][][][]")); nameTextField = new HintTextField("Filter Name", null); - add(nameTextField, "span 4, grow, wrap 2"); + add(nameTextField, "span 3, grow, wrap 2"); + JScrollPane scrollPane = new JScrollPane(nameTextField); addFilter(); // add/update filter - JButton addButton = new JButton("Add Filter"); + JButton addButton = new JButton(); + addButton.setIcon(UiUtils.getImageIcon("icon_add.png", 20)); addButton.addActionListener(e -> handleAddClicked()); - add(addButton, "skip 2, wrap"); + //add(addButton, "skip 3, wrap"); } private void addFilter() { @@ -59,19 +62,38 @@ private void addFilter() { add(filterPanel.columnComboBox, ""); add(filterPanel.expressionComboBox, ""); - add(filterPanel.valueField, "grow, wrap"); + add(filterPanel.valueField, "grow, wmin 150"); + add(filterPanel.deleteButton, "wrap"); + revalidate(); + repaint(); - filterList.add(filterPanel); + filterPanel.deleteButton.addActionListener(actionEvent -> { + deletePanel(filterPanel); + }); + + panelList.add(filterPanel); } private void handleAddClicked() { + addFilter(); + } + private void deletePanel(FilterPanel filterPanel) { + panelList.remove(filterPanel); + remove(filterPanel.columnComboBox); + remove(filterPanel.expressionComboBox); + remove(filterPanel.valueField); + remove(filterPanel.deleteButton); + + revalidate(); + repaint(); } public static class FilterPanel { private JComboBox columnComboBox; private JComboBox expressionComboBox; - private JTextField valueField; + private HintTextField valueField; + private JButton deleteButton; public FilterPanel() { // column @@ -85,7 +107,10 @@ public FilterPanel() { int exprIndex = ArrayUtils.indexOf(expressions, LogFilter.Expression.STARTS_WITH); expressionComboBox.setSelectedIndex(exprIndex); - valueField = new JTextField(); + deleteButton = new JButton(); + deleteButton.setIcon(UiUtils.getImageIcon("icon_delete.png", 20)); + + valueField = new HintTextField("Value", null); } } diff --git a/src/main/java/com/jpage4500/devicemanager/ui/views/CustomTable.java b/src/main/java/com/jpage4500/devicemanager/ui/views/CustomTable.java index b8fb1f4..2e70c7b 100644 --- a/src/main/java/com/jpage4500/devicemanager/ui/views/CustomTable.java +++ b/src/main/java/com/jpage4500/devicemanager/ui/views/CustomTable.java @@ -353,6 +353,10 @@ public boolean restoreTable() { TableColumnModel columnModel = getColumnModel(); // 1) backup columns to ColumnDetails for (ColumnDetails details : detailsList) { + if (details.name == null) { + log.debug("restoreTable: invalid: {}", GsonHelper.toJson(details)); + continue; + } details.column = getColumnByName(details.name); if (details.column != null) { columnModel.removeColumn(details.column); @@ -396,7 +400,7 @@ public TableColumn getColumnByName(String searchName) { return column; } } - log.error("getColumnByName: NOT_FOUND:{}", searchName); + log.error("getColumnByName: NOT_FOUND:{}, {}", searchName, Utils.getStackTraceString()); return null; } @@ -426,7 +430,11 @@ public void saveTable() { details.userPos = i; details.modelPos = column.getModelIndex(); details.width = column.getWidth(); - int maxWidth = column.getMaxWidth(); + //int maxWidth = column.getMaxWidth(); + if (details.name == null) { + log.debug("saveTable: invalid name:{} ({})", GsonHelper.toJson(details), prefKey); + continue; + } detailList.add(details); //log.trace("persist: {}, pos:{}, i:{}, w:{}, max:{}", details.header, i, details.modelPos, details.width, details.maxWidth); } diff --git a/src/main/java/com/jpage4500/devicemanager/utils/Utils.java b/src/main/java/com/jpage4500/devicemanager/utils/Utils.java index 416fb4e..9cafde9 100644 --- a/src/main/java/com/jpage4500/devicemanager/utils/Utils.java +++ b/src/main/java/com/jpage4500/devicemanager/utils/Utils.java @@ -145,4 +145,23 @@ public static CompareResult compareVersion(String v1, String v2) { return CompareResult.VERSION_EQUALS; } + public static String getStackTraceString() { + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < stackTrace.length; i++) { + // ignore THIS call + if (i < 2) continue; + StackTraceElement element = stackTrace[i]; + //String className = element.getClassName(); + String fileName = element.getFileName(); + int lineNumber = element.getLineNumber(); + // only show this app's classes + if (fileName == null || lineNumber == -1) continue; + if (!sb.isEmpty()) sb.append(", "); + sb.append("{" + fileName + ", " + element.getMethodName() + ", " + lineNumber + "}"); + } + + return sb.toString(); + } + } diff --git a/src/main/resources/images/system_tray.png b/src/main/resources/images/system_tray.png new file mode 100644 index 0000000000000000000000000000000000000000..c59838febc1b3ffe7c2e442bf87c83912e5834fc GIT binary patch literal 14529 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 LnoO0HS@8b>h$(Me literal 0 HcmV?d00001