Простой редактор текста by java through design

Всем привет. Для того чтобы писать код нужен удобный инструмент, кто-то пользуется IDE, кто-то редакторами текста Emacs, VIM. Зачастую, чтобы программисту было удобно, разработчику надо соблюсти много нюансов, тоесть в одном случае есть ускорение разработки, но решение тяжеловесное, в другом случае, разработчик, чтобы пользоваться редактором начинает писать код, чтобы писать код. Конечно это крайности, и проще поставить IDE/VSCode/etc… , но что если можно найти эту золотую середину?!

Неоднократно автор пытался делать текстовый редактор, но что-то было всё не то, пока не вспомнил, что есть старый добрый SWING, и java by design иногда круто и интересно.

В этой статье хочу показать как получилось минимальными усилиями сделать то, что на С или С++ потребует больших знаний без QT/GTK/etc…

Начнем!

Пользуюсь системой FreeBSD 14.2 Release

 % uname -rv
14.2-RELEASE FreeBSD 14.2-RELEASE releng/14.2-n269506-c8918d6c7412 GENERIC

Как поставить java (на всякий случай)

% pkg install openjdk17-17.0.13+11.1 openjdk17-jre-17.0.13+11.1
коммеентарий-строка#в /etc/fstab надо дополнить следующее и перезагрузиться или примонтировать чего не достаточно
fdesc	/dev/fd		fdescfs		rw	0	0
proc	/proc	procfs	rw	0	0

На всякий случай как скомпилировать программу

 % cat > main.java
class Main{
public static void main(String[] args){
	System.out.println("Hello World");
}
}
^C
 % javac main.java
 % java main.java
Hello World
 % 
 % cat > MANIFEST.MF
Manifest-Version: 1.0
Main-Class: Main
^C
 % jar -cvfm app.jar MANIFEST.MF *.class;
added manifest
adding: Main.class(in = 413) (out= 285)(deflated 30%)
 % java -jar ./app.jar
Hello World
 % 

Создадим скрипт на сборку

 % cat > compile.sh
javac Main.java; jar -cvfm app.jar MANIFEST.MF *.class; java -jar ./app.jar  
^C
 % cat compile.sh 
javac Main.java; jar -cvfm app.jar MANIFEST.MF *.class; java -jar ./app.jar
 % chmod +x compile.sh

Приступим к подходу шаблона над текстовым редактором

В ресурсах указано чем я пользовался java-swing-create-a-simple-text-editor.

давайте рассмотрим псевдокод того к чему надо приблизиться,

{
  Scroll nScrolls;
  MenubarWithItemsOrCli barTop,barBottom;
  Frame frame;
  TextArea text,numbers,cli;//stringsWithMetadata
  Process p;
  Prompt prompt;
  Command command;
}=> superClass
{
  load barTop(MenuAndItems);
  load Group(Scroll(numbers,text));
  load barBottom(Scroll(cli));
  connectAllLoadingToFrame(barTop,GroupWithOneScroll,barBottom);
  setCursorInValidPostoTextArea();
  setSettingsForFrame;
  Show();
}=> мы имеем строку куда вводятся символы и число строк
{}//Events
{}//ProcessIfNeedProvide
{
  call Editor;
}

давайте проинициализируем суперКласс

import java.awt.*;
import javax.swing.*;
import javax.swing.event.*;
import java.io.*;
import java.awt.event.*;
import javax.swing.plaf.metal.*;
import javax.swing.text.*;
import java.io.IOException;

class Editor extends JFrame implements ActionListener {
    // Text component
    private JTextArea t;                                     //code
    private JTextArea t1;                                    //lineNumbers
    private JTextArea t2;                                    //mini cli
    // Frame
    private JFrame f;                                        //frame-likewWindow
    private StringBuilder LineN;                             //string
    private String prompter=" \n % ";                        //prompt
    private String commander;                                //command after prompt
    private int LineNN=1;                                    //number of lines on doc
    // Constructor
    public Editor() 
    {
        // Create a frame
    	JFrame.setDefaultLookAndFeelDecorated(true);
        f = new JFrame("editor");
        try {
            // Set metal look and feel
            UIManager.setLookAndFeel("javax.swing.plaf.metal.MetalLookAndFeel");
            // Set theme to ocean
            MetalLookAndFeel.setCurrentTheme(new OceanTheme());//set theme read in doc
        }
        catch (Exception e) {
        }
    	//config jtextarea for numberLines
        t1 = new JTextArea("1\n",2,1);//start jtextarea with a1,a2,a3 - text , row, collumns
    	t1.setEditable(false);//disable edit
    	//config jtextarea for textSpace
        t = new JTextArea("111",21,20);
    	t.setTabSize(1);//1 tab
    	t.setCaretPosition(t.getDocument().getLength());//set cursor position in doc
    	t.addKeyListener(new CounterLines());//connect listen keyboard
	

	// Create a menubar
        JMenuBar mb = new JMenuBar();//menubar top
        // Create amenu for menu
        JMenu m1 = new JMenu("File");//menu for file
        // Create menu items
        JMenuItem mi1 = new JMenuItem("New");//item for file
        JMenuItem mi2 = new JMenuItem("Open");//item for file
        JMenuItem mi3 = new JMenuItem("Save");//item for file
        JMenuItem mi9 = new JMenuItem("Print");//item for file
        // Add action listener
        mi1.addActionListener(this);//connect event
        mi2.addActionListener(this);
        mi3.addActionListener(this);
        mi9.addActionListener(this);
        m1.add(mi1);//connect to File
        m1.add(mi2);
        m1.add(mi3);
        m1.add(mi9);
        // Create amenu for menu
        JMenu m2 = new JMenu("Edit");//menu for Edit
        // Create menu items
        JMenuItem mi4 = new JMenuItem("cut");//item for edit
        JMenuItem mi5 = new JMenuItem("copy");//item for edit
        JMenuItem mi6 = new JMenuItem("paste");//item for edit
        // Add action listener
        mi4.addActionListener(this);//connect event
        mi5.addActionListener(this);
        mi6.addActionListener(this);
        m2.add(mi4);//connect to Edit
        m2.add(mi5);
        m2.add(mi6);
        JMenu mc = new JMenu("Close");//menu button for close instant

    	mc.addMouseListener(new ExitAction());//connect event

        mb.add(m1);//connect all menu to menubar-top
        mb.add(m2);
        mb.add(mc);

    	f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//exit on button close X

    	JMenuBar mbConsole = new JMenuBar();//create menubar-bottom for cli
    	t2 = new JTextArea(prompter,10,20);//create jtextarea with a1 a2 a3
    	t2.addKeyListener(new CmrPrompter());//connect keyevent
    	mbConsole.add(t2);//connect jtextarea-cli to bottom menubar

    	//collors for all jtextareas t=code t1=numbers t2=cli
    	Color c = new Color(0,0,0,255);
    	Color cf = new Color(200,200,200,255);
    	t.setBackground(c);
    	t.setForeground(cf);
    	Color c1 = new Color(50,50,50,255);
    	Color cf1 = new Color(200,200,200,255);
    	t1.setBackground(c1);
    	t1.setForeground(cf1);
    	Color c2 = new Color(60,60,60,255);
    	Color cf2 = new Color(200,200,200,255);
    	t2.setBackground(c2);
    	t2.setForeground(cf2);
	
        JPanel panel = new JPanel();//create panel
    	panel.setLayout(new BorderLayout());//create template for complians

	
    	panel.add(t1, BorderLayout.WEST);//left because numbers
        panel.add(t, BorderLayout.CENTER);//right extend to center 
    	
        f.add(mb,BorderLayout.NORTH);//menubar to up
    	f.add(new JScrollPane(mbConsole),BorderLayout.SOUTH);//set scroll to cli and set bottom position
	
    	f.add(new JScrollPane(panel));//connect all panel to frame
	
        f.setSize(1200, 900);//set size wxh
    	centreWindow(f);//set center position
        f.show();//show()

    	t.requestFocus();//fix some situation with current pos and doc
    	t.setCaretPosition(t.getText().length());//fix some situation with cursor
    }
  //используем performs
   // If a button is pressed
    public void actionPerformed(ActionEvent e)//actions for items
    {
        String s = e.getActionCommand();
 
        if (s.equals("cut")) {
            t.cut();
        }
        else if (s.equals("copy")) {
            t.copy();
        }
        else if (s.equals("paste")) {
            t.paste();
        }
        else if (s.equals("Save")) {
            // Create an object of JFileChooser class
            JFileChooser j = new JFileChooser("f:");
 
            // Invoke the showsSaveDialog function to show the save dialog
            int r = j.showSaveDialog(null);
 
            if (r == JFileChooser.APPROVE_OPTION) {
 
                // Set the label to the path of the selected directory
                File fi = new File(j.getSelectedFile().getAbsolutePath());
 
                try {
                    // Create a file writer
                    FileWriter wr = new FileWriter(fi, false);
 
                    // Create buffered writer to write
                    BufferedWriter w = new BufferedWriter(wr);
 
                    // Write
                    w.write(t.getText());
 
                    w.flush();
                    w.close();
                }
                catch (Exception evt) {
                    JOptionPane.showMessageDialog(f, evt.getMessage());
                }
            }
            // If the user cancelled the operation
            else
                JOptionPane.showMessageDialog(f, "the user cancelled the operation");
        }
        else if (s.equals("Print")) {
            try {
                // print the file
                t.print();
            }
            catch (Exception evt) {
                JOptionPane.showMessageDialog(f, evt.getMessage());
            }
        }
        else if (s.equals("Open")) {
            // Create an object of JFileChooser class
            JFileChooser j = new JFileChooser("f:");
 
            // Invoke the showsOpenDialog function to show the save dialog
            int r = j.showOpenDialog(null);
 
            // If the user selects a file
            if (r == JFileChooser.APPROVE_OPTION) {
                // Set the label to the path of the selected directory
                File fi = new File(j.getSelectedFile().getAbsolutePath());
 
                try {
                    // String
                    String s1 = "", sl = "";
 
                    // File reader
                    FileReader fr = new FileReader(fi);
 
                    // Buffered reader
                    BufferedReader br = new BufferedReader(fr);
 
                    // Initialize sl
                    sl = br.readLine();
 
                    // Take the input from the file
                    while ((s1 = br.readLine()) != null) {
                        sl = sl + "\n" + s1;
            			LineNN+=1;//set linenumbers while if line
            			t1.append(""+LineNN);
            			t1.append(" \n");
                    }
 
                    // Set the text
                    t.setText(sl);
                }
                catch (Exception evt) {
                    JOptionPane.showMessageDialog(f, evt.getMessage());
                }
            }
            // If the user cancelled the operation
            else
                JOptionPane.showMessageDialog(f, "the user cancelled the operation");
        }
        else if (s.equals("New")) {
            t.setText("");
        }
        else if (s.equals("Close")) {
        }
	repaint();//
	revalidate();
    }
  //выход по Close
      //exit after clicked on Close Menu!=item
    class ExitAction extends MouseInputAdapter {
    	public void mouseClicked(MouseEvent mouseEvent) {
	    System.exit(0);
	}
    }
  //подсчет строк и применение в буффере счетчика строк
      class CounterLines extends KeyAdapter {//listen keyboard event for enter if enter add \n +i to numberLines
      	public void keyPressed(KeyEvent e) {
    	    if(e.getKeyCode() == KeyEvent.VK_ENTER){
        		LineNN+=1;
        		t1.append(""+LineNN);
        		t1.append(" \n");
    	    }
    	}
    }
  //установим окно в центр
      public static void centreWindow(Window frame) {//set center Window
    	Dimension dimension = Toolkit.getDefaultToolkit().getScreenSize();//get size
    	int x = (int) ((dimension.getWidth() - frame.getWidth()) / 2);//
    	int y = (int) ((dimension.getHeight() - frame.getHeight()) / 2);
    	frame.setLocation(x, y);
    }
  //поток где проводится команда
      class CmrPrompter extends KeyAdapter{//cmd\n - \n emulate enter
    	public void keyPressed(KeyEvent e){
    	    if(e.getKeyCode() == KeyEvent.VK_ENTER){
        		Runtime runtime = Runtime.getRuntime();//default process

        		//get token from doc https://godbolt.org/z/9e5W3zf81
        		String s1 = t2.getText();
        		String parts[] = s1.split("\n");
        		int sz = parts.length;
        		String part1 = parts[sz-1];
        		String parts2[] = part1.split("% ");
        		int sz1 = parts2.length;

        		commander=parts2[sz1-1];//set command token
    		try {//try
    		    Process pR = runtime.exec(commander);//call program

    		    BufferedReader cmdLineOut = new BufferedReader(new InputStreamReader(pR.getInputStream()));
    		    PrintWriter cmdLineIn = new PrintWriter(pR.getOutputStream());
    		    String s = null;
    		    while((s=cmdLineOut.readLine())!=null){//read line by line
        			t2.append("\n");
        			t2.append(s);
        			//System.out.println(cmdLineIn.toString());//commentary dont touching
    		    }
    		}
    		catch (IOException e1){
    		    e1.printStackTrace();
    		}
		commander= "";//free command string
		
		t2.append("\n");//set to the end//combo div-s
		t2.append(" % ");//set to the end//combo div-s

		t2.setText(t2.getText().substring(0,t2.getText().lastIndexOf("\r\n")));//set cursor after prompt 
		System.gc();//just
	    }
    }
  }
      // Main class
    public static void main(String[] args)
    {
        Editor e = new Editor();//jump to editor
	
    }
}
  

Этот код решает на стадии инициализации группирование обьектов, а именно — выставление двух елементов — буффер числа строк и текстовое поле пользователя в 1 скролл, выставление верхнего бара, применение в нижнем баре текстовое поле для использования в нём проведение пользовательских комманд.

В стадии использования как редактор он пока простенький, на первых подходах свою задачу выполняет.

если скомпилировать этот код и запустить

% ./compile.sh 
Note: Editor.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.
added manifest
adding: Editor.class(in = 6437) (out= 3606)(deflated 43%)
adding: Editor$1.class(in = 767) (out= 521)(deflated 32%)
adding: Editor$CmrPrompter.class(in = 1966) (out= 1124)(deflated 42%)
adding: Editor$CounterLines.class(in = 1129) (out= 658)(deflated 41%)
adding: Editor$ExitAction.class(in = 489) (out= 347)(deflated 29%)
adding: Editor$SMenuListener.class(in = 626) (out= 386)(deflated 38%)
adding: Editor$TextAreaOutputStream.class(in = 951) (out= 566)(deflated 40%)
adding: Editor$TextAreaOutputStream$1.class(in = 781) (out= 461)(deflated 40%)
adding: SMenuListener.class(in = 606) (out= 356)(deflated 41%)
запустится редактор
запустится редактор

Важный момент автор заметил при проведении процесса выпадение без остановки

Exception in thread "AWT-EventQueue-0" java.lang.StringIndexOutOfBoundsException: begin 0, end -1, length 73

это происходит в строке, где я решаю очистить строку от последнего елемента чтобы после проведения комманды курсор был как в терминале на своём месте, я пока пропустил это предупреждение.

t2.setText(t2.getText().substring(0,t2.getText().lastIndexOf("\r\n"))); //set cursor after prompt 

Теперь мы знаем как сделать простой редактор. А вам приходилось делать текстовый редактор? Поделитесь об этом в комментарии

Ресурсы:

JTextArea — разбор java-swing-create-a-simple-text-editor

BorderLayout

Runtime — java-runtime-getruntime-method

swing-jtextfield

abstract, Class, subclasses

© Habrahabr.ru