Add features such as tabs for editing multiple files, new file, open, save, cut, copy and paste to the text editor you learned to code in the first part.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 |
package texteditor; import java.awt.BorderLayout; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JTabbedPane; import javax.swing.KeyStroke; import javax.swing.UIManager; import javax.swing.UnsupportedLookAndFeelException; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; public class TextEditor { static JTabbedPane tabs; static String directory = "c:"; static DocListener docListener; public static void main(String args[]) { //Create JFrame JFrame window = new JFrame("Text Editor"); //Close program when the window is closed window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //Set default Windows appearance try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (UnsupportedLookAndFeelException e) { e.printStackTrace(); } Actions actions = new Actions(); //Create JPanel JPanel mainPanel = new JPanel(new BorderLayout()); //Add Text Editor tabs = new JTabbedPane(); //Optional - Custom UI for close button on tabs tabs.setUI(new CustomTabbedPaneUI()); Editor editor1 = new Editor(25, 60, "New File"); docListener = new DocListener(); editor1.getDocument().addDocumentListener(docListener); tabs.addTab(editor1.name+(editor1.saved ? "" : "*")+" ", editor1); mainPanel.add(tabs); //Add Menu JMenuBar menuBar = new JMenuBar(); addFileMenu(menuBar, actions); addEditMenu(menuBar, actions); window.setJMenuBar(menuBar); //Add JPanel to JFrame window.getContentPane().add(mainPanel, BorderLayout.CENTER); //Size JFrame window.pack(); window.setVisible(true); } private static void addFileMenu(JMenuBar menu, ActionListener a) { //Create a menu JMenu file = new JMenu("File"); //Add items JMenuItem newFile = new JMenuItem("New"); JMenuItem open = new JMenuItem("Open"); JMenuItem save = new JMenuItem("Save"); //Set action listener to assign functions newFile.addActionListener(a); open.addActionListener(a); save.addActionListener(a); //Set shortcuts key + modifier (that Toolkit thing) newFile.setAccelerator(KeyStroke.getKeyStroke('N', Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); open.setAccelerator(KeyStroke.getKeyStroke('O', Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); save.setAccelerator(KeyStroke.getKeyStroke('S', Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); //Add items to menu file.add(newFile); file.add(open); file.add(save); //Add menu to bar menu.add(file); } private static void addEditMenu(JMenuBar menu, ActionListener a) { JMenu file = new JMenu("Edit"); JMenuItem cut = new JMenuItem("Cut"); JMenuItem copy = new JMenuItem("Copy"); JMenuItem paste = new JMenuItem("Paste"); cut.addActionListener(a); copy.addActionListener(a); paste.addActionListener(a); cut.setAccelerator(KeyStroke.getKeyStroke('X', Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); copy.setAccelerator(KeyStroke.getKeyStroke('C', Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); paste.setAccelerator(KeyStroke.getKeyStroke('V', Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); file.add(cut); file.add(copy); file.add(paste); menu.add(file); } public static void Save() { //Create dialog window to save the file if(tabs.getTabCount()<1) return; Editor ed = ((Editor)tabs.getSelectedComponent()); directory = ed.getDirectory(); JFileChooser chooser = new JFileChooser(directory); //Open it and check if the users pressed Save if(chooser.showSaveDialog(null)==JFileChooser.APPROVE_OPTION) { directory = chooser.getSelectedFile().getAbsolutePath(); if(directory.lastIndexOf('.')==-1) directory += ".txt"; ed.setDirectory(directory); //Get information of the file to create File file = new File(directory); ed.setName(file.getName()); try { //Create the file FileWriter writer = new FileWriter(file); //Create a buffer to the file BufferedWriter bw = new BufferedWriter(writer); //Write things in it bw.write(ed.getText()); //Force all data in the buffer to be written into the file bw.flush(); //Close the buffer bw.close(); ed.setSaved(true); tabs.setTitleAt(tabs.getSelectedIndex(), ed.name+(ed.saved ? "" : "*")+" "); } catch (IOException e) { //Error JOptionPane.showMessageDialog(null, e.getMessage()); } } } private static void Open() { //Create dialog window to open a file JFileChooser chooser = new JFileChooser(directory); //Open it and check if the users pressed Open if(chooser.showOpenDialog(null)==JFileChooser.APPROVE_OPTION) { directory = chooser.getSelectedFile().getAbsolutePath(); //Get information of the file to create File file = new File(directory); try { //Open the file FileReader reader = new FileReader(file); //Create a buffer to the file BufferedReader br = new BufferedReader(reader); //Read each line until the file ends String text = "", s; while((s = br.readLine())!=null) text += s + '\n'; //Close the buffer br.close(); Editor ed1 = new Editor(25, 60, file.getName()); ed1.setDirectory(directory); ed1.setSaved(true); tabs.addTab(ed1.name+(ed1.saved ? "" : "*")+" ", ed1); ed1.setText(text); ed1.getDocument().addDocumentListener(docListener); } catch (IOException e) { //Error JOptionPane.showMessageDialog(null, e.getMessage()); } } } private static class Actions implements ActionListener { @Override public void actionPerformed(ActionEvent arg0) { //Get who fired this action String s = arg0.getActionCommand(); switch(s) { case "Cut": ((Editor)tabs.getSelectedComponent()).cut(); break; case "Copy": ((Editor)tabs.getSelectedComponent()).copy(); break; case "Paste": ((Editor)tabs.getSelectedComponent()).paste(); break; case "New": Editor ed1 = new Editor(25, 60, "New File"); ed1.getDocument().addDocumentListener(docListener); tabs.addTab(ed1.name+(ed1.saved ? "" : "*")+" ", ed1); break; case "Open": Open(); break; case "Save": Save(); break; } } } private static class DocListener implements DocumentListener { @Override public void removeUpdate(DocumentEvent e) { Editor ed = ((Editor)tabs.getSelectedComponent()); ed.setSaved(false); tabs.setTitleAt(tabs.getSelectedIndex(), ed.name+(ed.saved ? "" : "*")+" "); } @Override public void insertUpdate(DocumentEvent e) { Editor ed = ((Editor)tabs.getSelectedComponent()); ed.setSaved(false); tabs.setTitleAt(tabs.getSelectedIndex(), ed.name+(ed.saved ? "" : "*")+" "); } @Override public void changedUpdate(DocumentEvent arg0) { Editor ed = ((Editor)tabs.getSelectedComponent()); ed.setSaved(false); tabs.setTitleAt(tabs.getSelectedIndex(), ed.name+(ed.saved ? "" : "*")+" "); } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
package texteditor; import java.awt.Font; import javax.swing.JTextArea; public class Editor extends JTextArea{ String name; String directory = ""; boolean saved = false; public Editor(int width, int height, String n) { super(width, height); this.setFont(new Font("Times new roman", Font.PLAIN, 17)); name = n; } public String getName() { return name; } public String getDirectory() { return name; } public boolean isSaved() { return saved; } public void setName(String n) { name = n; } public void setDirectory(String d) { directory = d; } public void setSaved(boolean s) { saved = s; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
package texteditor; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Rectangle; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import javax.swing.JOptionPane; import javax.swing.JTabbedPane; import javax.swing.plaf.basic.BasicTabbedPaneUI; public class CustomTabbedPaneUI extends BasicTabbedPaneUI { Rectangle xRect; protected void installListeners() { super.installListeners(); tabPane.addMouseListener(new MyMouseHandler()); } protected void paintTab(Graphics g, int tabPlacement, Rectangle[] rects, int tabIndex, Rectangle iconRect, Rectangle textRect) { super.paintTab(g, tabPlacement, rects, tabIndex, iconRect, textRect); Font f = g.getFont(); g.setFont(new Font("Courier", Font.BOLD, 14)); FontMetrics fm = g.getFontMetrics(g.getFont()); g.setColor(Color.red); int charWidth = fm.charWidth('x'); int maxAscent = fm.getMaxAscent(); g.drawString("x", textRect.x + textRect.width-7, textRect.y + textRect.height - 3); xRect = new Rectangle(textRect.x+textRect.width-9, textRect.y+textRect.height-maxAscent, charWidth+2, maxAscent-1); g.setFont(f); } public class MyMouseHandler extends MouseAdapter { public void mousePressed(MouseEvent e) { if (xRect.contains(e.getPoint())) { JTabbedPane tabPane = (JTabbedPane)e.getSource(); int tabIndex = tabForCoordinate(tabPane, e.getX(), e.getY()); Editor ed1 = (Editor) tabPane.getComponent(tabIndex); if(ed1.getText().length()>0) { if(ed1.isSaved()) { tabPane.remove(tabIndex); }else { switch(JOptionPane.showConfirmDialog(null, "Do you want to save the current file?")) { case JOptionPane.YES_OPTION: TextEditor.Save(); tabPane.remove(tabIndex); break; case JOptionPane.NO_OPTION: tabPane.remove(tabIndex); break; } } }else tabPane.remove(tabIndex); } } } } |
In the first part you learned how to make a text editor using Java Swing’s libraries.
However there’s one thing we haven’t covered yet: features.
If you followed that tutorial, now you’ve probably got a text editor that works… at least until you start pressing buttons.
You made a complete template for your text editor last time, so now let’s add some functions to it.
Functions such as:
- Creating a new file
- Opening a file
- Saving a file
- Cutting stuff
- Copying stuff
- Pasting stuff
1. The Action and Document Listener
A listener in Java is an object that contains a bunch of functions called when a certain event happens.
A key pressed, a button clicked or a document modified may generate a response from a listener.
All listener classes are defined as abstract.
This means that the class determines all the listener’s functions, but not what they do.
This is why you can’t instance objects directly from an abstract class and even if you could they wouldn’t do anything.
It’s up to you deciding what those functions should do. How?
Creating a new class that implements those functions determined in the abstract class.
Then you can override those to define their behaviour yourself.
Since I created those classes in the same file of the main function, I had to make them static, because a static function can’t call not static functions, classes or variables defined in the same file.
ActionListener
An action listener is called when the user clicks on an element.
I wrote an Actions class that implements the ActionListener class and which assigns a function to each item in the menus.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
private static class Actions implements ActionListener { @Override public void actionPerformed(ActionEvent arg0) { //Get who fired this action String s = arg0.getActionCommand(); switch(s) { case "Cut": ((Editor)tabs.getSelectedComponent()).cut(); break; case "Copy": ((Editor)tabs.getSelectedComponent()).copy(); break; case "Paste": ((Editor)tabs.getSelectedComponent()).paste(); break; case "New": Editor ed1 = new Editor(25, 60, "New File"); ed1.getDocument().addDocumentListener(docListener); tabs.addTab(ed1.name+(ed1.saved ? "" : "*")+" ", ed1); break; case "Open": Open(); break; case "Save": Save(); break; } } } |
The s string contains the name of the element that called the action listener, or, in other words, the one with which the user has interacted.
From that, using a switch statement, we can decide what to do.
The first three function are very simple: the get the component (a text editor area) in the current tab and cast it to an Editor object, so that the computer knows that that object belongs to that class and it allows us to use its functions.
The fourth case creates a new Editor object, it assigns a document listener to it and it adds it to the tab container as we saw previously – and that we can use because it’s defined outside any function.
The fifth and sixth cases use two custom function that we’re going to see later – like everything in this post apparently.
DocumentListener
A document listener waits for any change to a document.
If you type a letter in a text area, a document listener is called.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
private static class DocListener implements DocumentListener { @Override public void removeUpdate(DocumentEvent e) { Editor ed = ((Editor)tabs.getSelectedComponent()); ed.setSaved(false); tabs.setTitleAt(tabs.getSelectedIndex(), ed.name+(ed.saved ? "" : "*")+" "); } @Override public void insertUpdate(DocumentEvent e) { Editor ed = ((Editor)tabs.getSelectedComponent()); ed.setSaved(false); tabs.setTitleAt(tabs.getSelectedIndex(), ed.name+(ed.saved ? "" : "*")+" "); } @Override public void changedUpdate(DocumentEvent arg0) { Editor ed = ((Editor)tabs.getSelectedComponent()); ed.setSaved(false); tabs.setTitleAt(tabs.getSelectedIndex(), ed.name+(ed.saved ? "" : "*")+" "); } } |
All three functions of this class contain the same code, because I want it to be executed when any type of change happens.
When one of them is called, it gets the Editor object from the current tab and it sets it as not saved, then it changes the title of the tab.
I could have added an asterisk directly, but I prefer see if everything worked.
The three spaces I added at the end of the tab’s name are used to leave some space for the “x” which closes the tab and that I’m going to explain later.
2. The Save and Open functions
The Save and the Open functions are similar, but not in the same way addFileMenu and addEditMenu – explained in the previous post – are.
They both link the text editor with files, but in opposite directions – as you’ve probably expected.
Futhermore, since we want them to remember the last directory opened for a file, they’re more complex than needed for basic working.
But you know, you’re here for some real programming and not for that simple coding that anyone out there can teach.
The Save function
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
public static void Save() { //First Part //Create dialog window to save the file if(tabs.getTabCount()<1) return; Editor ed = ((Editor)tabs.getSelectedComponent()); directory = ed.getDirectory(); JFileChooser chooser = new JFileChooser(directory); //Second Part //Open it and check if the users pressed Save if(chooser.showSaveDialog(null)==JFileChooser.APPROVE_OPTION) { directory = chooser.getSelectedFile().getAbsolutePath(); if(directory.lastIndexOf('.')==-1) directory += ".txt"; ed.setDirectory(directory); //Get information of the file to create File file = new File(directory); ed.setName(file.getName()); //Third Part try { //Create the file FileWriter writer = new FileWriter(file); //Create a buffer to the file BufferedWriter bw = new BufferedWriter(writer); //Write things in it bw.write(ed.getText()); //Force all data in the buffer to be written into the file bw.flush(); //Close the buffer bw.close(); ed.setSaved(true); tabs.setTitleAt(tabs.getSelectedIndex(), ed.name+(ed.saved ? "" : "*")+" "); } catch (IOException e) { //Error JOptionPane.showMessageDialog(null, e.getMessage()); } } } |
Let’s divide this piece of code in three parts:
- Preparation
- File choice
- Actual saving
Preparation
In the first part we see if there are any open tabs, otherwise there’s nothing to save.
If so, we get the Editor object from the current tab, because we need its directory variable’s value and we’re also going to use it later.
We’re going to use its directory variable’s value to open a file chooser (JFileChooser) – picture below – at the last file’s location.
This can be obtained either by a previous saving or when the file is opened, otherwise it remains blank – or any other default value specified in the Editor class file.
File choice
The second part starts with an if statement that does two things:
First, it shows the file chooser created in the previous line (chooser.showSaveDialog(null)
), then it checks if the user clicked “Save” (JFileChooser.APPROVE_OPTION
).
Yes, the program does stop while the file chooser is open. Yes, even if the function is in another statement.
Now, if the user pressed “Save”, the program gets the choosen directory and set it to a file variable (a variable declared outside any function that can be used everywhere within its file) called directory.
1 2 3 4 |
if(directory.lastIndexOf('.')==-1) directory += ".txt"; |
Those lines above check if the user forgot to specify the file’s extension and if so, they set it to “.txt” (a simple text file).
How do they know that the extension is missing? File extensions are separated from file names through a dot, so if no dots mean no extensions.
lastIndexOf('.')
looks for the last occurrens of a character or a string in anothe string. If it doesn’t find anything, it returns -1.
The next line sets the Editor object’s directory variable to the newly selected directory, in case it changed.
Then we create a File object, which is used to manage the file at the specified location – or it creates one if it doesn’t exist.
From it we can get the file’s name without much effort and we can set it as the Editor object’s name value, so that we can use it to set the file’s tab’s name later.
Actual saving
The third part begins with a try-catch statement, which prevents the program to crash if an error occurs during the writing of the file.
→This article by BeginnersBook explains what a try-catch statement is.
Most of the code in this part is sufficiently commented, but I want you to focus on these things anyway:
- The difference between FileWriter and BufferedWriter, that is well explained here.
- The difference of data being in a buffer vs in an actual file, which you would know if you read my article about files in C
- The line
ed.setSaved(true);
, which prevents the next line from adding an asterisk to the file’s name in its tab’s heading. - The line
JOptionPane.showMessageDialog(null, e.getMessage());
that shows a dialog window if an error occured.
The Open function
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
private static void Open() { //Create dialog window to open a file JFileChooser chooser = new JFileChooser(directory); //Open it and check if the users pressed Open if(chooser.showOpenDialog(null)==JFileChooser.APPROVE_OPTION) { directory = chooser.getSelectedFile().getAbsolutePath(); //Get information of the file to create File file = new File(directory); try { //Open the file FileReader reader = new FileReader(file); //Create a buffer to the file BufferedReader br = new BufferedReader(reader); //Read each line until the file ends String text = "", s; while((s = br.readLine())!=null) text += s + '\n'; //Close the buffer br.close(); //Create a new Editor object Editor ed1 = new Editor(25, 60, file.getName()); ed1.setDirectory(directory); ed1.setSaved(true); //Add a new tab with that object tabs.addTab(ed1.name+(ed1.saved ? "" : "*")+" ", ed1); ed1.setText(text); ed1.getDocument().addDocumentListener(docListener); } catch (IOException e) { //Error JOptionPane.showMessageDialog(null, e.getMessage()); } } } |
I think the Open function is a bit more complicated than the Save one, because we have to create a new tab with a new Editor object – as far as that can be considered “complicated”.
On the other hand, however, I’ve already explained a lot of similar thing for the Save function, such as the file chooser.
Therefore I’m going to take into consideration only two parts of code:
- Reading file content
- Creating a new tab
Reading file content
1 2 3 4 5 |
String text = "", s; while((s = br.readLine())!=null) text += s + '\n'; |
The key to understand this piece of code is knowing that the readLine() function returns null when the reader reaches the end of a file.
So, we have two String variables, one for storing the whole text and the other to store temporarily each line.
Until the end of the file is reached, a new line is read and added to the first variable.
Creating a new tab
1 2 3 4 5 6 7 8 |
Editor ed1 = new Editor(25, 60, file.getName()); ed1.setDirectory(directory); ed1.setSaved(true); tabs.addTab(ed1.name+(ed1.saved ? "" : "*")+" ", ed1); ed1.setText(text); ed1.getDocument().addDocumentListener(docListener); |
Once the program has read and closed the file, it has to create a new tab, with a new text area and fill it with the content stored in the text string.
In order to do that, you must first create a new Editor object using the file’s name – 1st line.
Then you set the file’s directory and you set it as saved, because we’ve just opened it.
In the fourth line we add a tab as we did in the last post.
Finally, we set the text and the document listener, using another file variable called docListener.
You can see how I created the two listener objects in the main function.
3. Extras
There are two more things I want to point out: close buttons for tabs and “look and feel”.
Let’s start from the simplest.
Look and feel is simply the appereance of a window and its elements.
For example, Windows and Java has two different look and feels, as you can see below.
The other piece of code is a class that modifies the default tab heading to add a close button.
I’ve taken the code from esus.com and I’ve modified it so that the program asks if you want to save an usaved file that you’re about to close.
It’s a bit harder to understand since it works with Java AWT – another set of graphic libraries – and we haven’t covered it yet.
All its code is in the CustomTabbedPaneUI class and it can be used by adding this simple line:
1 |
tabs.setUI(new CustomTabbedPaneUI()); |
Conclusion
I know, I know, even if I divided this tutorial in two parts, they’re both long… but it’s worth it.
We didn’t make the usual simple and useless text editor, but an actual working program – probably better than the classic notepad.
Do you want to learn the basics of programming? It doesn’t matter if you want to code in Java, C++ or whatever, start from here: Learn C programming.
If you have any questions, ask them in a comment and if you liked the article, please share it.
Anyway, the post is finished, have a good day and we will see next week!
From Zephyro it’s all, Bye!