diff --git a/.gitignore b/.gitignore index f87907a0..cb7684a3 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ build/ .classpath .project .settings +Thumbs.db diff --git a/build.gradle b/build.gradle index fd968490..524224f2 100644 --- a/build.gradle +++ b/build.gradle @@ -78,6 +78,7 @@ dependencies { implementation group: 'org.jfree', name: 'jfreechart', version: '1.5.3' implementation group: 'org.apache.poi', name: 'poi', version: '5.1.0' implementation group: 'org.apache.poi', name: 'poi-ooxml', version: '5.1.0' + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version:'2.11.2' implementation group: 'org.drjekyll', name: 'fontchooser', version: '2.4' implementation group: 'net.java.dev.jna', name: 'jna', version: '5.10.0' diff --git a/src/main/kx/c.java b/src/main/kx/c.java index a835be6f..9d69440d 100755 --- a/src/main/kx/c.java +++ b/src/main/kx/c.java @@ -590,7 +590,8 @@ private K.KBase k(ProgressCallback progress) throws K4Exception, IOException { if (b[0] == -128) { j = 1; - throw new K4Exception(rs().toString()); + //showType=false because an error is NOT a symbol, this would cause confusion with novice users who can't tell the difference + throw new K4Exception(rs().toString(false)); } return r(); } diff --git a/src/log4j2.xml b/src/main/log4j2.xml similarity index 100% rename from src/log4j2.xml rename to src/main/log4j2.xml diff --git a/src/main/studio/core/Studio.java b/src/main/studio/core/Studio.java index abe1d09a..05d8a757 100755 --- a/src/main/studio/core/Studio.java +++ b/src/main/studio/core/Studio.java @@ -189,7 +189,13 @@ private static void initTaskbarIcon() { Class taskbarClass = Class.forName("java.awt.Taskbar"); Object taskbar = taskbarClass.getDeclaredMethod("getTaskbar").invoke(taskbarClass); taskbarClass.getDeclaredMethod("setIconImage", Image.class).invoke(taskbar, Util.LOGO_ICON.getImage()); - } catch (Exception e) { + }catch (java.lang.reflect.InvocationTargetException e) { + if (e.getCause() instanceof java.lang.UnsupportedOperationException) { + //no need to report - always happens on Windows + } else { + log.error("Failed to set Taskbar icon", e); + } + }catch (Exception e) { log.error("Failed to set Taskbar icon", e); } } diff --git a/src/main/studio/kdb/Config.java b/src/main/studio/kdb/Config.java index 1fa57fe5..af61b2ee 100755 --- a/src/main/studio/kdb/Config.java +++ b/src/main/studio/kdb/Config.java @@ -6,6 +6,7 @@ import studio.core.Credentials; import studio.core.DefaultAuthenticationMechanism; import studio.ui.ServerList; +import studio.ui.Util; import studio.utils.HistoricalList; import studio.utils.LineEnding; import studio.utils.QConnection; @@ -22,6 +23,10 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.swing.tree.TreeNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.JsonNode; public class Config { private static final Logger log = LogManager.getLogger(); @@ -85,6 +90,7 @@ private enum ConfigType { STRING, INT, DOUBLE, BOOLEAN, FONT, BOUNDS, COLOR, ENU public static final String OPEN_FILE_CHOOSER = configDefault("openFileChooser", ConfigType.FILE_CHOOSER, new FileChooserConfig()); public static final String SAVE_FILE_CHOOSER = configDefault("saveFileChooser", ConfigType.FILE_CHOOSER, new FileChooserConfig()); public static final String EXPORT_FILE_CHOOSER = configDefault("exportFileChooser", ConfigType.FILE_CHOOSER, new FileChooserConfig()); + public static final String SERVERLIST_FILE_CHOOSER = configDefault("serverListFileChooser", ConfigType.FILE_CHOOSER, new FileChooserConfig()); private enum FontStyle { Plain(Font.PLAIN), Bold(Font.BOLD), Italic(Font.ITALIC), ItalicAndBold(Font.BOLD|Font.ITALIC); @@ -96,6 +102,9 @@ private enum FontStyle { public int getStyle() { return style; } + public static FontStyle getStyle(int fontStyle) { + return FontStyle.values()[fontStyle]; + } } // The folder is also referenced in lon4j2.xml config @@ -164,20 +173,20 @@ public synchronized static void setEnvironment(String env) { } } - public Workspace loadWorkspace() { - Workspace workspace = new Workspace(); - File workspaceFile = new File(getWorkspaceFilename()); - if (workspaceFile.exists()) { - try (InputStream inp = new FileInputStream(workspaceFile)) { - Properties p = new Properties(); - p.load(inp); - workspace.load(p); - } catch (IOException e) { - log.error("Can't load workspace", e); - } - } - return workspace; - } + public Workspace loadWorkspace() { + Workspace workspace = new Workspace(); + File workspaceFile = new File(getWorkspaceFilename()); + if (workspaceFile.exists()) { + try (InputStream inp = new FileInputStream(workspaceFile)) { + Properties p = new Properties(); + p.load(inp); + workspace.load(p); + } catch (IOException e) { + log.error("Can't load workspace", e); + } + } + return workspace; + } public void saveWorkspace(Workspace workspace) { try { @@ -306,6 +315,33 @@ private void init(String filename, Properties properties) { } } + if (!Files.exists(file) && Util.WINDOWS) { + log.info("Config not found in userprofile. Trying legacy path."); + //Old Java versions returned a different place for user.home on Windows. + //A user upgrading from such old directory would suddenly "lose" their config. + String oldpath = null; + try { + Process process = Runtime.getRuntime().exec("reg query \"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\" /v Desktop"); + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + String line = null; + while ((line = reader.readLine()) != null) { + if (line.contains("Desktop") && line.contains("REG_SZ")) { + // Desktop REG_SZ \\path\to\Desktop + String[] tokens = line.split("[ \t]"); + int tc=0; + for (int i=0; i 0) ++tc; + if (tc==3) oldpath = tokens[i]; + } + } + } + } catch (IOException e) { + //ignore + } + log.info("Old path: "+oldpath); + if (oldpath != null) file = Paths.get(oldpath.substring(0,oldpath.lastIndexOf('\\'))+"\\.studioforkdb\\studio.properties"); + } + if (properties != null) { p = (Properties) properties.clone(); } else { @@ -407,6 +443,148 @@ public void save() { } } + public Object serverTreeToObj(ServerTreeNode root) { + //converts the server tree to an object that can be saved into JSON + LinkedHashMap result = new LinkedHashMap<>(); + result.put("name", root.getName()); + if(root.isFolder()) { + ArrayList children = new ArrayList<>(); + result.put("children", children); + for (Enumeration e = root.children(); e.hasMoreElements();) { + children.add(serverTreeToObj((ServerTreeNode) e.nextElement())); + } + } + return result; + } + + public void exportServerListToJSON(File f) { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.configure(SerializationFeature.INDENT_OUTPUT, true); + Map cfg = new LinkedHashMap<>(); + ArrayList> svs = new ArrayList<>(); + for (Server s : servers.values()) { + LinkedHashMap ps = new LinkedHashMap<>(); + svs.add(ps); + ps.put("name", s.getName()); + ps.put("host", s.getHost()); + ps.put("port", s.getPort()); + ps.put("username", s.getUsername()); + ps.put("password", s.getPassword()); + ps.put("useTls", s.getUseTLS()); + ps.put("authMethod", s.getAuthenticationMechanism()); + ArrayList color = new ArrayList<>(3); + Color bgc = s.getBackgroundColor(); + color.add(bgc.getRed()); + color.add(bgc.getGreen()); + color.add(bgc.getBlue()); + ps.put("color", color); + } + cfg.put("servers",svs); + cfg.put("serverTree", serverTreeToObj(serverTree)); + try { + FileWriter sw = new FileWriter(f); + objectMapper.writeValue(sw, cfg); + } catch(IOException e) { + e.printStackTrace(System.err); + } + } + + private void importServerTreeFromJSON(HashMap serverMap, boolean isRoot, JsonNode jn, ServerTreeNode tn) { + if (jn.has("children")) { //is a folder + ServerTreeNode ntn = tn; + if (!isRoot) { + String folderName = jn.get("name").asText(""); + ntn = tn.getChild(folderName); + if (ntn == null) { + ntn = new ServerTreeNode(folderName); + tn.add(ntn); + } + }; + JsonNode children = jn.get("children"); + if (children.isArray()) { + for (JsonNode child : (Iterable) ()->children.elements()) { + importServerTreeFromJSON(serverMap, false, child, ntn); + } + } + } else { + if (jn.has("name")) { + String name = jn.get("name").asText(""); + if (name.length() > 0) { + if (serverMap.containsKey(name)) { + Server s = serverMap.get(name); + s.setFolder(tn); + addServer(s); + serverMap.remove(s); + } + } + } + } + } + + public String importServerListFromJSON(File f) { + ObjectMapper objectMapper = new ObjectMapper(); + StringBuilder sb = new StringBuilder(); + ArrayList alreadyExist = new ArrayList<>(); + ArrayList noName = new ArrayList<>(); + try { + JsonNode root = objectMapper.readTree(f); + if (!root.isObject()) return "JSON root node is not an object"; + if (!root.has("servers")) return "JSON root node doesn't have a \"servers\" property"; + if (!root.has("serverTree")) return "JSON root node doesn't have a \"serverTree\" property"; + JsonNode serversNode = root.get("servers"); + JsonNode serverTreeNode = root.get("serverTree"); + if (!serversNode.isArray()) return "\"servers\" node is not an array"; + HashSet existingServers = new HashSet<>(); + for (Server s : servers.values()) existingServers.add(s.getName()); + HashMap serverMap = new HashMap<>(); + int i=0; + for (JsonNode serverNode : (Iterable) ()->serversNode.elements()) { + if (!serverNode.isObject()) { + sb.append("Non-object found inside \"servers\" array at index "+i+"\n"); + } else if (!serverNode.has("name")) { + sb.append("Server at index "+i+" has no name\n"); + } else { + String sname = serverNode.get("name").asText(); + if (sname.length() == 0) { + noName.add(i); + } else if (existingServers.contains(sname)) { + alreadyExist.add(sname); + } else { + Server s = new Server(); + s.setName(sname); + if (serverNode.has("host")) s.setHost(serverNode.get("host").asText("")); + if (serverNode.has("port")) s.setPort(serverNode.get("port").asInt(0)); + if (serverNode.has("username")) s.setUsername(serverNode.get("username").asText("")); + if (serverNode.has("password")) s.setPassword(serverNode.get("password").asText("")); + if (serverNode.has("useTls")) s.setUseTLS(serverNode.get("useTls").asBoolean(false)); + if (serverNode.has("authMethod")) s.setAuthenticationMechanism(serverNode.get("authMethod").asText("")); + if (serverNode.has("color")) { + JsonNode color = serverNode.get("color"); + if (color.isArray() && color.size() >= 3) { + s.setBackgroundColor(new Color(color.get(0).asInt(255),color.get(1).asInt(255),color.get(2).asInt(255))); + } + } + serverMap.put(sname, s); + } + } + ++i; + } + if (serverTreeNode.isObject()) { + importServerTreeFromJSON(serverMap, true, serverTreeNode, serverTree); + } + if (0n.toString()).collect(Collectors.joining("/")); } + + public String getName() { + return isFolder() ? getFolder() : getServer().getName(); + } } diff --git a/src/main/studio/ui/EditorPane.java b/src/main/studio/ui/EditorPane.java index d70b2b64..89a80da9 100644 --- a/src/main/studio/ui/EditorPane.java +++ b/src/main/studio/ui/EditorPane.java @@ -14,6 +14,7 @@ public class EditorPane extends JPanel { private final RSyntaxTextArea textArea; + private final RTextScrollPane scrollPane; private final MinSizeLabel lblRowCol; private final MinSizeLabel lblInsStatus; private final JLabel lblStatus; @@ -66,7 +67,7 @@ public void keyReleased(KeyEvent e) { Font font = Config.getInstance().getFont(Config.FONT_EDITOR); textArea.setFont(font); - RTextScrollPane scrollPane = new RTextScrollPane(textArea); + scrollPane = new RTextScrollPane(textArea); scrollPane.getGutter().setLineNumberFont(font); searchPanel = new SearchPanel(this); @@ -129,4 +130,10 @@ private void setBorder(JComponent component) { ) ); } + + public void setTextAreaFont(Font font) { //don't call this setFont, it leads to an error + textArea.setFont(font); + scrollPane.getGutter().setLineNumberFont(font); + } + } diff --git a/src/main/studio/ui/EditorTab.java b/src/main/studio/ui/EditorTab.java index 83832145..deca04e8 100644 --- a/src/main/studio/ui/EditorTab.java +++ b/src/main/studio/ui/EditorTab.java @@ -10,6 +10,7 @@ import studio.utils.FileWatcher; import studio.utils.LineEnding; +import java.awt.Font; import javax.swing.text.BadLocationException; import javax.swing.text.Document; import javax.swing.text.JTextComponent; @@ -264,4 +265,8 @@ public void fileModified(Path path) { editorPane.setTemporaryStatus("Reload of " + filename + " failed"); } } + + public void setTextAreaFont(Font font) { //don't call this setFont, it leads to an error + editorPane.setTextAreaFont(font); + } } diff --git a/src/main/studio/ui/ServerForm.java b/src/main/studio/ui/ServerForm.java index 7fb852ac..c31bdd8d 100644 --- a/src/main/studio/ui/ServerForm.java +++ b/src/main/studio/ui/ServerForm.java @@ -20,6 +20,7 @@ public class ServerForm extends EscapeDialog { private Server s; + private String originalName; public ServerForm(Window frame, String title, Server server){ super(frame, title); @@ -27,12 +28,13 @@ public ServerForm(Window frame, String title, Server server){ initComponents(); + originalName = s.getName(); logicalName.setText(s.getName()); hostname.setText(s.getHost()); username.setText(s.getUsername()); port.setText(""+s.getPort()); password.setText(s.getPassword()); - jCheckBox2.setSelected(s.getUseTLS()); + useTls.setSelected(s.getUseTLS()); DefaultComboBoxModel dcbm= (DefaultComboBoxModel)authenticationMechanism.getModel(); String [] am; am = AuthenticationManager.getInstance().getAuthenticationMechanisms(); @@ -52,7 +54,7 @@ public ServerForm(Window frame, String title, Server server){ logicalName.setToolTipText("The logical name for the server"); hostname.setToolTipText("The hostname or ip address for the server"); port.setToolTipText("The port for the server"); - jCheckBox2.setToolTipText("Use TLS for a secure connection"); + useTls.setToolTipText("Use TLS for a secure connection"); username.setToolTipText("The username used to connect to the server"); password.setToolTipText("The password used to connect to the server"); authenticationMechanism.setToolTipText("The authentication mechanism to use"); @@ -94,14 +96,14 @@ private void initComponents() { jSeparator1 = new javax.swing.JSeparator(); jSeparator2 = new javax.swing.JSeparator(); password = new javax.swing.JPasswordField(); - jLabel1 = new javax.swing.JLabel(); + colorLabel = new javax.swing.JLabel(); jSeparator3 = new javax.swing.JSeparator(); EditColorButton = new javax.swing.JButton(); SampleTextOnBackgroundTextField = new javax.swing.JTextField(); authenticationMechanism = new javax.swing.JComboBox(); - passwordLabel1 = new javax.swing.JLabel(); - jCheckBox2 = new javax.swing.JCheckBox(); - jLabel2 = new javax.swing.JLabel(); + authMethodLabel = new javax.swing.JLabel(); + useTls = new javax.swing.JCheckBox(); + tlsLabel = new javax.swing.JLabel(); logicalNameLabel.setText("Name"); @@ -127,7 +129,7 @@ public void actionPerformed(java.awt.event.ActionEvent evt) { } }); - jLabel1.setText("Color"); + colorLabel.setText("Color"); EditColorButton.setText("Edit Color"); EditColorButton.addActionListener(new java.awt.event.ActionListener() { @@ -143,9 +145,9 @@ public void actionPerformed(java.awt.event.ActionEvent evt) { } }); - passwordLabel1.setText("Auth. Method"); + authMethodLabel.setText("Auth. Method"); - jLabel2.setText("Use TLS"); + tlsLabel.setText("Use TLS"); GroupLayout layout = new GroupLayout(getContentPane()); getContentPane().setLayout(layout); @@ -157,11 +159,11 @@ public void actionPerformed(java.awt.event.ActionEvent evt) { .addComponent(logicalNameLabel) .addComponent(hostnameLabel) .addComponent(portLabel) - .addComponent(jLabel2) + .addComponent(tlsLabel) .addComponent(usernameLabel) .addComponent(passwordLabel) - .addComponent(passwordLabel1) - .addComponent(jLabel1)) + .addComponent(authMethodLabel) + .addComponent(colorLabel)) .addPreferredGap(RELATED, 21, Short.MAX_VALUE) .addGroup(layout.createParallelGroup(LEADING) .addGroup(layout.createSequentialGroup() @@ -177,7 +179,7 @@ public void actionPerformed(java.awt.event.ActionEvent evt) { .addComponent(authenticationMechanism, 0, 418, Short.MAX_VALUE) .addComponent(password, DEFAULT_SIZE, 418, Short.MAX_VALUE) .addComponent(username, DEFAULT_SIZE, 418, Short.MAX_VALUE) - .addComponent(jCheckBox2) + .addComponent(useTls) .addComponent(port, DEFAULT_SIZE, 418, Short.MAX_VALUE) .addComponent(hostname, DEFAULT_SIZE, 418, Short.MAX_VALUE) .addComponent(logicalName, DEFAULT_SIZE, 418, Short.MAX_VALUE)) @@ -214,8 +216,8 @@ public void actionPerformed(java.awt.event.ActionEvent evt) { .addComponent(port, PREFERRED_SIZE, DEFAULT_SIZE, PREFERRED_SIZE)) .addPreferredGap(RELATED) .addGroup(layout.createParallelGroup(LEADING) - .addComponent(jLabel2, TRAILING) - .addComponent(jCheckBox2, TRAILING)) + .addComponent(tlsLabel, TRAILING) + .addComponent(useTls, TRAILING)) .addPreferredGap(RELATED) .addComponent(jSeparator2, PREFERRED_SIZE, 10, PREFERRED_SIZE) .addPreferredGap(RELATED) @@ -229,12 +231,12 @@ public void actionPerformed(java.awt.event.ActionEvent evt) { .addPreferredGap(RELATED) .addGroup(layout.createParallelGroup(BASELINE) .addComponent(authenticationMechanism, PREFERRED_SIZE, DEFAULT_SIZE, PREFERRED_SIZE) - .addComponent(passwordLabel1)) + .addComponent(authMethodLabel)) .addPreferredGap(RELATED) .addComponent(jSeparator3, PREFERRED_SIZE, 10, PREFERRED_SIZE) .addPreferredGap(RELATED) .addGroup(layout.createParallelGroup(BASELINE) - .addComponent(jLabel1) + .addComponent(colorLabel) .addComponent(SampleTextOnBackgroundTextField, PREFERRED_SIZE, DEFAULT_SIZE, PREFERRED_SIZE)) .addPreferredGap(RELATED) .addGroup(layout.createParallelGroup(BASELINE) @@ -251,23 +253,32 @@ private void onOk(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_onOk username.setText(username.getText().trim()); port.setText(port.getText().trim()); password.setText(new String(password.getPassword()).trim()); - - if(logicalName.getText().length() == 0) + String newName = logicalName.getText().trim(); + if(newName.length() == 0) { StudioOptionPane.showError(this, "The server's name cannot be empty", "Studio for kdb+"); logicalName.requestFocus(); return; - } + } + + boolean clash = false; + if (!originalName.equals(newName)) { + for (Server server : Config.getInstance().getServers()) { + if (newName.equals(server.getName())) { + clash = true; + break; + } + } + } - boolean clash=false; - if( clash) + if(clash) { StudioOptionPane.showError(this, "A server already exists with that name.", "Studio for kdb+"); logicalName.requestFocus(); return; } else { - s.setName(logicalName.getText().trim()); + s.setName(newName); s.setHost(hostname.getText().trim()); s.setUsername(username.getText().trim()); if(port.getText().length() == 0) @@ -276,7 +287,7 @@ private void onOk(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_onOk s.setPort(Integer.parseInt(port.getText())); s.setPassword(new String(password.getPassword()).trim()); - s.setUseTLS(jCheckBox2.isSelected()); + s.setUseTLS(useTls.isSelected()); DefaultComboBoxModel dcbm= (DefaultComboBoxModel)authenticationMechanism.getModel(); s.setAuthenticationMechanism((String)dcbm.getSelectedItem()); } @@ -322,9 +333,9 @@ private void SampleTextOnBackgroundTextFieldActionPerformed(java.awt.event.Actio private javax.swing.JButton cancelButton; private javax.swing.JTextField hostname; private javax.swing.JLabel hostnameLabel; - private javax.swing.JCheckBox jCheckBox2; - private javax.swing.JLabel jLabel1; - private javax.swing.JLabel jLabel2; + private javax.swing.JCheckBox useTls; + private javax.swing.JLabel colorLabel; + private javax.swing.JLabel tlsLabel; private javax.swing.JSeparator jSeparator1; private javax.swing.JSeparator jSeparator2; private javax.swing.JSeparator jSeparator3; @@ -333,7 +344,7 @@ private void SampleTextOnBackgroundTextFieldActionPerformed(java.awt.event.Actio private javax.swing.JButton okButton; private javax.swing.JPasswordField password; private javax.swing.JLabel passwordLabel; - private javax.swing.JLabel passwordLabel1; + private javax.swing.JLabel authMethodLabel; private javax.swing.JTextField port; private javax.swing.JLabel portLabel; private javax.swing.JTextField username; diff --git a/src/main/studio/ui/ServerList.java b/src/main/studio/ui/ServerList.java index 44454656..21f00442 100644 --- a/src/main/studio/ui/ServerList.java +++ b/src/main/studio/ui/ServerList.java @@ -5,20 +5,24 @@ import studio.kdb.Config; import studio.kdb.Server; import studio.kdb.ServerTreeNode; +import studio.ui.action.JSONServerList; import javax.swing.*; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.event.TreeExpansionEvent; import javax.swing.event.TreeExpansionListener; +import javax.swing.filechooser.FileNameExtensionFilter; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; import java.awt.*; import java.awt.event.*; +import java.io.*; import java.util.ArrayList; import java.util.Enumeration; +import java.util.function.*; import java.util.HashSet; import java.util.StringTokenizer; import java.util.List; @@ -27,6 +31,7 @@ public class ServerList extends EscapeDialog implements TreeExpansionListener { private static final Logger log = LogManager.getLogger(); + private StudioPanel studioPanel; private JPanel contentPane; private JTabbedPane tabbedPane; private JList serverHistoryList; @@ -35,6 +40,7 @@ public class ServerList extends EscapeDialog implements TreeExpansionListener { private DefaultTreeModel treeModel; private JTextField filter; private JToggleButton tglBtnBoxTree; + private JMenuBar menubar; private boolean ignoreExpansionListener = false; private java.util.Set expandedPath = new HashSet<>(); @@ -43,15 +49,18 @@ public class ServerList extends EscapeDialog implements TreeExpansionListener { private Server selectedServer; private Server activeServer; private ServerTreeNode serverTree, root; + private ServerTreeNode pickedUpNode = null; private JPopupMenu popupMenu; private UserAction selectAction, removeAction, insertFolderAction, insertServerAction, addServerBeforeAction, addServerAfterAction, - addFolderBeforeAction, addFolderAfterAction; + addFolderBeforeAction, addFolderAfterAction, + pickUpAction, dropIntoAction, dropAboveAction, dropBelowAction, + importFromJSONAction, exportToJSONAction; public static final int DEFAULT_WIDTH = 300; - public static final int DEFAULT_HEIGHT = 400; + public static final int DEFAULT_HEIGHT = 410; private static final String JTABBED_TREE_LABEL = "Servers - tree"; private static final String JTABBED_LIST_LABEL = "Servers - list"; @@ -59,8 +68,9 @@ public class ServerList extends EscapeDialog implements TreeExpansionListener { private static final int menuShortcutKeyMask = java.awt.Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(); private final KeyStroke TREE_VIEW_KEYSTROKE = KeyStroke.getKeyStroke(KeyEvent.VK_T, menuShortcutKeyMask); - public ServerList(JFrame parent) { + public ServerList(JFrame parent, StudioPanel studioPanel) { super(parent, "Server List"); + this.studioPanel = studioPanel; initComponents(); } @@ -346,6 +356,14 @@ public void mouseClicked(MouseEvent e) { initActions(); initPopupMenu(); + + JMenuBar menubar = new JMenuBar(); + JMenu menu = new JMenu(I18n.getString("File")); + menu.setMnemonic(KeyEvent.VK_F); + menu.add(new JMenuItem(importFromJSONAction)); + menu.add(new JMenuItem(exportToJSONAction)); + menubar.add(menu); + setJMenuBar(menubar); } private void initActions() { @@ -366,6 +384,20 @@ private void initActions() { addFolderAfterAction = UserAction.create("Add Folder After", "Add Folder after selected node", KeyEvent.VK_A, e -> addNode(true, AddNodeLocation.AFTER)); + pickUpAction = UserAction.create("Pick up", "Pick up node for move", + KeyEvent.VK_P, e -> pickUpNode()); + dropIntoAction = UserAction.create("Drop Into", "Move node into the folder", + KeyEvent.VK_D, e -> dropNode(AddNodeLocation.INSERT)); + dropAboveAction = UserAction.create("Drop Above", "Move node above selected node", + KeyEvent.VK_O, e -> dropNode(AddNodeLocation.BEFORE)); + dropBelowAction = UserAction.create("Drop Below", "Move node below selected node", + KeyEvent.VK_L, e -> dropNode(AddNodeLocation.AFTER)); + + importFromJSONAction = UserAction.create("Import from JSON...", "Import server list from JSON", + KeyEvent.VK_I, e -> JSONServerList.importFromJSON(this, studioPanel)); + exportToJSONAction = UserAction.create("Export to JSON...", "Export server list to JSON", + KeyEvent.VK_E, e -> JSONServerList.exportToJSON(this)); + UserAction toggleAction = UserAction.create("toggle", e-> toggleTreeListView()); UserAction focusTreeAction = UserAction.create("focus tree", e-> tree.requestFocusInWindow()); UserAction selectServerFromHistory = UserAction.create("select from history", e -> selectServerFromHistory()); @@ -407,13 +439,19 @@ private void handlePopup(MouseEvent e) { addServerBeforeAction.setEnabled(false); addServerAfterAction.setEnabled(false); removeAction.setEnabled(false); + pickUpAction.setEnabled(false); + dropIntoAction.setEnabled(false); + dropAboveAction.setEnabled(false); + dropBelowAction.setEnabled(false); } else { boolean isFolder = ((ServerTreeNode) path.getLastPathComponent()).isFolder(); - + boolean canDrop = pickedUpNode != null; selectAction.setEnabled(!isFolder); insertServerAction.setEnabled(isFolder); insertFolderAction.setEnabled(isFolder); - + dropIntoAction.setEnabled(isFolder && canDrop); + dropAboveAction.setEnabled(canDrop); + dropBelowAction.setEnabled(canDrop); addFolderBeforeAction.setEnabled(!empty); addFolderAfterAction.setEnabled(!empty); addServerBeforeAction.setEnabled(!empty); @@ -437,6 +475,11 @@ private void initPopupMenu() { popupMenu.add(addServerAfterAction); popupMenu.add(new JSeparator()); popupMenu.add(removeAction); + popupMenu.add(new JSeparator()); + popupMenu.add(pickUpAction); + popupMenu.add(dropIntoAction); + popupMenu.add(dropAboveAction); + popupMenu.add(dropBelowAction); } private void removeNode() { @@ -460,10 +503,43 @@ private void removeNode() { TreePath treePath = new TreePath(path); tree.scrollPathToVisible(treePath); tree.setSelectionPath(treePath); + if (node == pickedUpNode) pickedUpNode = null; } private enum AddNodeLocation {INSERT, BEFORE, AFTER}; + private void addExistingNode(ServerTreeNode target, ServerTreeNode newNode, AddNodeLocation location) { + ServerTreeNode parent = location == AddNodeLocation.INSERT ? target : (ServerTreeNode)target.getParent(); + int index; + if (location == AddNodeLocation.INSERT) { + index = target.getChildCount(); + } else { + index = parent.getIndex(target); + if (location == AddNodeLocation.AFTER) index++; + int prevIndex = parent.getIndex(newNode); + if (prevIndex > -1 && prevIndex < index) index--; //remove + insert into same folder + } + + parent.insert(newNode, index); + try { + Config.getInstance().setServerTree(serverTree); + } catch (IllegalArgumentException exception) { + serverTree = Config.getInstance().getServerTree(); + System.err.println("Error adding new node: " + exception); + exception.printStackTrace(System.err); + JOptionPane.showMessageDialog(this, "Error adding new node:\n" + exception.toString(), "Error", JOptionPane.ERROR_MESSAGE); + + } + refreshServers(); + + ServerTreeNode selNode = root.findPath(newNode.getPath()); + if (selNode != null) { + TreePath treePath = new TreePath(selNode.getPath()); + tree.scrollPathToVisible(treePath); + tree.setSelectionPath(treePath); + } + } + private void addNode(boolean folder, AddNodeLocation location) { ServerTreeNode selNode = (ServerTreeNode) tree.getLastSelectedPathComponent(); if (selNode == null) return; @@ -488,31 +564,32 @@ private void addNode(boolean folder, AddNodeLocation location) { server.setFolder(parent); newNode = new ServerTreeNode(server); } + addExistingNode(node, newNode, location); + } - int index; - if (location == AddNodeLocation.INSERT) { - index = node.getChildCount(); - } else { - index = parent.getIndex(node); - if (location == AddNodeLocation.AFTER) index++; - } + private void pickUpNode() { + pickedUpNode = (ServerTreeNode) tree.getLastSelectedPathComponent(); + } - parent.insert(newNode, index); - try { - Config.getInstance().setServerTree(serverTree); - } catch (IllegalArgumentException exception) { - serverTree = Config.getInstance().getServerTree(); - log.error("Error adding new node", exception); - StudioOptionPane.showError(this, "Error adding new node:\n" + exception, "Error"); + private void dropNode(AddNodeLocation location) { + if (pickedUpNode == null) return; + ServerTreeNode selNode = (ServerTreeNode) tree.getLastSelectedPathComponent(); + if (selNode == pickedUpNode) { + JOptionPane.showMessageDialog(this,"Cannot move a node relative to itself.","Error",JOptionPane.ERROR_MESSAGE,Util.ERROR_ICON); + return; } - refreshServers(); - - selNode = root.findPath(newNode.getPath()); - if (selNode != null) { - TreePath treePath = new TreePath(selNode.getPath()); - tree.scrollPathToVisible(treePath); - tree.setSelectionPath(treePath); + if (pickedUpNode.isFolder()) { + ServerTreeNode dropTarget = location == AddNodeLocation.INSERT ? selNode : (ServerTreeNode) selNode.getParent(); + while (dropTarget != root) { + if (dropTarget == pickedUpNode) { + JOptionPane.showMessageDialog(this,"Cannot move a folder into itself.","Error",JOptionPane.ERROR_MESSAGE,Util.ERROR_ICON); + return; + } + dropTarget = (ServerTreeNode) dropTarget.getParent(); + } } + addExistingNode(selNode, pickedUpNode, location); + studioPanel.updateServerComboBox(); } } diff --git a/src/main/studio/ui/SettingsDialog.java b/src/main/studio/ui/SettingsDialog.java index c2a16224..2fe7254f 100644 --- a/src/main/studio/ui/SettingsDialog.java +++ b/src/main/studio/ui/SettingsDialog.java @@ -37,6 +37,8 @@ public class SettingsDialog extends EscapeDialog { private JFormattedTextField txtEmulateDoubleClickTimeout; private JComboBox comboBoxExecAll; private JComboBox comboBoxLineEnding; + private JComboBox cbFontName; + private JSpinner spnFontSize; private JButton btnOk; private JButton btnCancel; @@ -136,6 +138,14 @@ public void align() { btnOk.requestFocusInWindow(); } + public String getFontName() { + return cbFontName.getSelectedItem().toString(); + } + + public int getFontSize() { + return (Integer)spnFontSize.getValue(); + } + private void initComponents() { txtUser = new JTextField(12); txtPassword = new JPasswordField(12); @@ -219,6 +229,13 @@ private void initComponents() { comboBoxLineEnding = new JComboBox<>(LineEnding.values()); comboBoxLineEnding.setSelectedItem(Config.getInstance().getEnum(Config.DEFAULT_LINE_ENDING)); + JLabel lblFontSize = new JLabel("Font:"); + spnFontSize = new JSpinner(new SpinnerNumberModel(Config.getInstance().getFontSize(), 8, 72, 1)); + + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + cbFontName = new JComboBox(ge.getAvailableFontFamilyNames()); + cbFontName.getModel().setSelectedItem(Config.getInstance().getFontName()); + JLabel lblAuthMechanism = new JLabel("Authentication:"); JLabel lblUser = new JLabel(" User:"); JLabel lblPassword = new JLabel(" Password:"); @@ -246,6 +263,7 @@ private void initComponents() { .addLineAndGlue(chBoxRTSAAnimateBracketMatching, chBoxRTSAHighlightCurrentLine, chBoxRTSAWordWrap) .addLineAndGlue(lblDefaultLineEnding, comboBoxLineEnding) .addLineAndGlue(lblExecAll, comboBoxExecAll) + .addLine(lblFontSize, spnFontSize, cbFontName) ); JPanel pnlResult = new JPanel(); diff --git a/src/main/studio/ui/StudioFileChooser.java b/src/main/studio/ui/StudioFileChooser.java new file mode 100755 index 00000000..dc44e535 --- /dev/null +++ b/src/main/studio/ui/StudioFileChooser.java @@ -0,0 +1,79 @@ +package studio.ui; + +import java.awt.Component; +import java.awt.Dimension; +import java.io.File; +import java.util.HashMap; +import java.util.Map; +import javax.swing.filechooser.FileFilter; +import javax.swing.filechooser.FileNameExtensionFilter; +import javax.swing.JFileChooser; + +import studio.kdb.Config; +import studio.kdb.FileChooserConfig; + +public class StudioFileChooser { + + private static final Config CONFIG = Config.getInstance(); + private static Map fileChooserMap = new HashMap<>(); + + public static File chooseFile(Component parent, String fileChooserType, int dialogType, String title, File defaultFile, FileFilter... filters) { + JFileChooser fileChooser = fileChooserMap.get(fileChooserType); + FileChooserConfig config = CONFIG.getFileChooserConfig(fileChooserType); + if (fileChooser == null) { + fileChooser = new JFileChooser(); + fileChooserMap.put(fileChooserType, fileChooser); + + if (title != null) fileChooser.setDialogTitle(title); + fileChooser.setDialogType(dialogType); + fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); + for (FileFilter ff: filters) { + fileChooser.addChoosableFileFilter(ff); + } + if (filters.length == 1) fileChooser.setFileFilter(filters[0]); + + if (defaultFile == null && ! config.getFilename().equals("")) { + defaultFile = new File(config.getFilename()); + } + + } + + if (defaultFile != null) { + fileChooser.setCurrentDirectory(defaultFile.getParentFile()); + fileChooser.setSelectedFile(defaultFile); + fileChooser.ensureFileIsVisible(defaultFile); + } + + Dimension preferredSize = config.getPreferredSize(); + if (preferredSize.width > 0 && preferredSize.height > 0) { + fileChooser.setPreferredSize(preferredSize); + } + + int option; + if (dialogType == JFileChooser.OPEN_DIALOG) option = fileChooser.showOpenDialog(parent); + else option = fileChooser.showSaveDialog(parent); + + File selectedFile = fileChooser.getSelectedFile(); + String filename = ""; + if (selectedFile != null) { + filename = selectedFile.getAbsolutePath(); + } + + if (dialogType == JFileChooser.SAVE_DIALOG && option == JFileChooser.APPROVE_OPTION) { + FileFilter ff = fileChooser.getFileFilter(); + if (ff instanceof FileNameExtensionFilter) { + String ext = "." + ((FileNameExtensionFilter) ff).getExtensions()[0]; + if (!filename.endsWith(ext)) { + filename = filename + ext; + selectedFile = new File(filename); + } + } + } + + config = new FileChooserConfig(filename, fileChooser.getSize()); + CONFIG.setFileChooserConfig(fileChooserType, config); + + return option == JFileChooser.APPROVE_OPTION ? selectedFile : null; + } + +} diff --git a/src/main/studio/ui/StudioPanel.java b/src/main/studio/ui/StudioPanel.java index 43a70c48..49bcdf43 100755 --- a/src/main/studio/ui/StudioPanel.java +++ b/src/main/studio/ui/StudioPanel.java @@ -9,6 +9,7 @@ import studio.core.Credentials; import studio.core.Studio; import studio.kdb.*; +import studio.ui.action.JSONServerList; import studio.ui.action.QPadImport; import studio.ui.action.QueryResult; import studio.ui.action.WorkspaceSaver; @@ -23,7 +24,6 @@ import javax.swing.*; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; -import javax.swing.filechooser.FileFilter; import javax.swing.filechooser.FileNameExtensionFilter; import javax.swing.plaf.basic.BasicSplitPaneUI; import javax.swing.table.TableModel; @@ -116,6 +116,8 @@ public class StudioPanel extends JPanel implements WindowListener { private UserAction toggleDividerOrientationAction; private UserAction minMaxDividerAction; private UserAction importFromQPadAction; + private UserAction importFromJSONAction; + private UserAction exportToJSONAction; private UserAction editServerAction; private UserAction addServerAction; private UserAction removeServerAction; @@ -126,8 +128,6 @@ public class StudioPanel extends JPanel implements WindowListener { private UserAction wordWrapAction; private JFrame frame; - private static Map fileChooserMap = new HashMap<>(); - private static List allPanels = new ArrayList<>(); private final List serverHistory; @@ -233,65 +233,6 @@ public void refreshActionState() { } } - private static File chooseFile(Component parent, String fileChooserType, int dialogType, String title, File defaultFile, FileFilter... filters) { - JFileChooser fileChooser = fileChooserMap.get(fileChooserType); - FileChooserConfig config = CONFIG.getFileChooserConfig(fileChooserType); - if (fileChooser == null) { - fileChooser = new JFileChooser(); - fileChooserMap.put(fileChooserType, fileChooser); - - if (title != null) fileChooser.setDialogTitle(title); - fileChooser.setDialogType(dialogType); - fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); - for (FileFilter ff: filters) { - fileChooser.addChoosableFileFilter(ff); - } - if (filters.length == 1) fileChooser.setFileFilter(filters[0]); - - if (defaultFile == null && ! config.getFilename().equals("")) { - defaultFile = new File(config.getFilename()); - } - - } - - if (defaultFile != null) { - fileChooser.setCurrentDirectory(defaultFile.getParentFile()); - fileChooser.setSelectedFile(defaultFile); - fileChooser.ensureFileIsVisible(defaultFile); - } - - Dimension preferredSize = config.getPreferredSize(); - if (preferredSize.width > 0 && preferredSize.height > 0) { - fileChooser.setPreferredSize(preferredSize); - } - - int option; - if (dialogType == JFileChooser.OPEN_DIALOG) option = fileChooser.showOpenDialog(parent); - else option = fileChooser.showSaveDialog(parent); - - File selectedFile = fileChooser.getSelectedFile(); - String filename = ""; - if (selectedFile != null) { - filename = selectedFile.getAbsolutePath(); - } - - if (dialogType == JFileChooser.SAVE_DIALOG && option == JFileChooser.APPROVE_OPTION) { - FileFilter ff = fileChooser.getFileFilter(); - if (ff instanceof FileNameExtensionFilter) { - String ext = "." + ((FileNameExtensionFilter) ff).getExtensions()[0]; - if (!filename.endsWith(ext)) { - filename = filename + ext; - selectedFile = new File(filename); - } - } - } - - config = new FileChooserConfig(filename, fileChooser.getSize()); - CONFIG.setFileChooserConfig(fileChooserType, config); - - return option == JFileChooser.APPROVE_OPTION ? selectedFile : null; - } - private void exportAsExcel(final String filename) { new ExcelExporter().exportTableX(frame,getSelectedTable(),new File(filename),false); } @@ -419,7 +360,7 @@ private void exportAsCSV(String filename) { private void export() { if (getSelectedTable() == null) return; - File file = chooseFile(this, Config.EXPORT_FILE_CHOOSER, JFileChooser.SAVE_DIALOG, "Export result set as", + File file = StudioFileChooser.chooseFile(this, Config.EXPORT_FILE_CHOOSER, JFileChooser.SAVE_DIALOG, "Export result set as", null, new FileNameExtensionFilter("csv (Comma delimited)", "csv"), new FileNameExtensionFilter("txt (Tab delimited)", "txt"), @@ -458,7 +399,7 @@ public void newFile() { } private void openFile() { - File file = chooseFile(this, Config.OPEN_FILE_CHOOSER, JFileChooser.OPEN_DIALOG, null, null, + File file = StudioFileChooser.chooseFile(this, Config.OPEN_FILE_CHOOSER, JFileChooser.OPEN_DIALOG, null, null, new FileNameExtensionFilter("q script", "q")); if (file == null) return; @@ -513,7 +454,7 @@ public boolean loadFile(String filename) { private static boolean saveAsFile(EditorTab editor) { String filename = editor.getFilename(); - File file = chooseFile(editor.getPanel(), Config.SAVE_FILE_CHOOSER, JFileChooser.SAVE_DIALOG, "Save script as", + File file = StudioFileChooser.chooseFile(editor.getPanel(), Config.SAVE_FILE_CHOOSER, JFileChooser.SAVE_DIALOG, "Save script as", filename == null ? null : new File(filename), new FileNameExtensionFilter("q script", "q")); @@ -597,6 +538,8 @@ private void setServer(Server server) { } private void initActions() { + StudioPanel thePanel = this; + cleanAction = UserAction.create("Clean", Util.NEW_DOCUMENT_ICON, "Clean editor script", KeyEvent.VK_N, null, e -> newFile()); @@ -648,6 +591,10 @@ private void initActions() { importFromQPadAction = UserAction.create("Import Servers from QPad...", null, "Import from Servers.cfg", KeyEvent.VK_I, null, e -> QPadImport.doImport(this)); + importFromJSONAction = UserAction.create("Import Servers from JSON...", "Import server list from JSON", + KeyEvent.VK_J, e -> JSONServerList.importFromJSON(this, this)); + exportToJSONAction = UserAction.create("Export Servers to JSON...", "Export server list to JSON", + KeyEvent.VK_X, e -> JSONServerList.exportToJSON(this)); editServerAction = UserAction.create(I18n.getString("Edit"), Util.SERVER_EDIT_ICON, "Edit the server details", KeyEvent.VK_E, null, e -> { @@ -860,6 +807,8 @@ public static void settings() { boolean changedEditor = CONFIG.setBoolean(Config.RSTA_ANIMATE_BRACKET_MATCHING, dialog.isAnimateBracketMatching()); changedEditor |= CONFIG.setBoolean(Config.RSTA_HIGHLIGHT_CURRENT_LINE, dialog.isHighlightCurrentLine()); changedEditor |= CONFIG.setBoolean(Config.RSTA_WORD_WRAP, dialog.isWordWrap()); + Font font = new Font(dialog.getFontName(), Font.PLAIN, dialog.getFontSize()); + changedEditor |= CONFIG.setFont(Config.FONT_EDITOR, font); if (changedEditor) { refreshEditorsSettings(); @@ -876,7 +825,6 @@ public static void settings() { StudioOptionPane.showMessage(activePanel.frame, "Look and Feel was changed. " + "New L&F will take effect on the next start up.", "Look and Feel Setting Changed"); } - activePanel.rebuildToolbar(); } @@ -889,13 +837,16 @@ private void toggleWordWrap() { } private static void refreshEditorsSettings() { + Font font = CONFIG.getFont(Config.FONT_EDITOR); for (StudioPanel panel: allPanels) { int count = panel.tabbedEditors.getTabCount(); for (int index=0; index= CONFIG.getResultTabsCount()) { - tabbedPane.remove(0); + KTableModel model = KTableModel.getModel(queryResult.getResult()); + TabPanel modelTab = null; + if (model != null) { + modelTab = new TabPanel(panel, queryResult, model); + modelTab.addInto(tabbedPane); + modelTab.setToolTipText(editor.getServer().getConnectionString()); } + TabPanel tab = new TabPanel(panel, queryResult, null); tab.addInto(tabbedPane); tab.setToolTipText(editor.getServer().getConnectionString()); + if (modelTab != null) { + tabbedPane.setSelectedComponent(modelTab); + } + while (tabbedPane.getTabCount() > CONFIG.getResultTabsCount()) { + tabbedPane.remove(0); + } } error = null; } catch (Throwable exc) { diff --git a/src/main/studio/ui/TabPanel.java b/src/main/studio/ui/TabPanel.java index 9576d8c9..f8b01009 100755 --- a/src/main/studio/ui/TabPanel.java +++ b/src/main/studio/ui/TabPanel.java @@ -22,11 +22,11 @@ public class TabPanel extends JPanel { private KFormatContext formatContext = new KFormatContext(KFormatContext.DEFAULT); private ResultType type; - public TabPanel(StudioPanel panel, QueryResult queryResult) { + public TabPanel(StudioPanel panel, QueryResult queryResult, KTableModel model) { this.panel = panel; this.queryResult = queryResult; this.result = queryResult.getResult(); - initComponents(); + initComponents(model); } public void setPanel(StudioPanel panel) { @@ -52,10 +52,9 @@ private void upload() { panel.executeK4Query(new K.KList(new K.Function("{x set y}"), new K.KSymbol(varName), result)); } - private void initComponents() { + private void initComponents(KTableModel model) { JComponent component; if (result != null) { - KTableModel model = KTableModel.getModel(result); if (model != null) { grid = new QGrid(panel, model); component = grid; diff --git a/src/main/studio/ui/action/JSONServerList.java b/src/main/studio/ui/action/JSONServerList.java new file mode 100755 index 00000000..9ae5eb37 --- /dev/null +++ b/src/main/studio/ui/action/JSONServerList.java @@ -0,0 +1,62 @@ +package studio.ui.action; + +import java.awt.Component; +import java.io.File; +import java.util.function.Consumer; +import javax.swing.filechooser.FileNameExtensionFilter; +import javax.swing.JFileChooser; +import javax.swing.JOptionPane; + +import studio.kdb.Config; +import studio.ui.StudioFileChooser; +import studio.ui.StudioPanel; +import studio.ui.Util; + +public class JSONServerList { + + private static void fileDialog(Component parent, String title, boolean isSave, Consumer fileOp) { + File file = StudioFileChooser.chooseFile(parent, + Config.SERVERLIST_FILE_CHOOSER, + isSave ? JFileChooser.SAVE_DIALOG : JFileChooser.OPEN_DIALOG, + title, + null, //defaultFile + new FileNameExtensionFilter("JSON file", "json")); + + if (file != null) { + try { + fileOp.accept(file); + } + catch (Exception e) { + e.printStackTrace(System.err); + JOptionPane.showMessageDialog(parent, + "Error in file operation:\n" + e, + "Studio for kdb+", + JOptionPane.ERROR_MESSAGE, + Util.ERROR_ICON); + } + } + } + + public static void exportToJSON(Component parent) { + fileDialog(parent, + "Export server list as", + true, + file->Config.getInstance().exportServerListToJSON(file)); + } + + public static void importFromJSON(Component parent, StudioPanel studioPanel) { + fileDialog(parent, + "Import server list from", + false, + file->{ + String errors = Config.getInstance().importServerListFromJSON(file); + if (0