[JAVA] Create a weekly git GUI client [5] First desktop app

It's not monthly, so it's safe. Basically the story of Swing.

Summary up to the previous issue

[\ 1 ] stage / unstage basic knowledge

5 things to do for line-by-line stage / unstage

1. Output diff as the original patch 2. Select the line you want to stage / unstage 3. Edit the body of hunk`` 4. Recalculate the hunk header`` 5. Apply the final patch

[\ 2 ] stage / unstage incomplete capture

It was impossible to use JGit, so I talked about using git.exe.

[\ 3 ] stage / unstage patch editing

3. Edit the body of hunk`` 4. Recalculate the hunk header``

[4] diff et al.

1. Output diff as the original patch

This issue

2. Select the line you want to stage / unstage

進捗.gif

However, it must be displayed in order to be selected. This time, it's about display.

Difficult to think about UI

As a GUI application

It seems that it is necessary to reflect such things in the design, I'm not basically thinking about it here. I want to make "my git, which I thought about"

Just think about it and be happy.

For example UITableView

The first thing I imagined was A collection-type view with sections like the iOS UITableView. But you and Java, this is the JVM.

As a UI for partial selection of diff with Swing quickly I chose JList because it seems to be the easiest. The diffs that are the source of the patch are displayed in a list line by line and selected.

I wanted to update row by row, so I made it JTable (described later), Since it is a JTable with only one column, the basic usage is the same.

Mo-like feeling

By the way, even if you display it in a list, you don't have to display the diff line by line. All you have to do is display the text inside each line of the list and let it be selected there. I naturally thought that the unit of selection was a row in a list.

Around this time, I feel like I'm stuck in mobile application development. (I basically live in iOS and Android native app development)

Stage / unstage trigger after selection

After selecting, you need an action that triggers stage / unstage. If you look at SourceTree, you'll see a button and a right-click menu. Right-clicking is a hassle to use regularly. It is also troublesome to think about the position and shape of the button.

That's why I decided to click the header part of the diff.

Diff display

hunk

It is easier to display the data obtained from git diff as it is, so let's display it in hunk units.

The diff header here has multiple lines for each file, From the line starting with diff --git to just before the header line of hunk. The hunk header here is one line starting with @@.

<diff-per-file>
	<diff-header />
	<hunk>
		<hunk-header />
		<hunk-body />
	</hunk>
	<hunk>
		<hunk-header />
		<hunk-body />
	</hunk>
</diff-per-file>

<!--There may be no hunk such as images-->
<diff-per-file>
	<diff-header />
</diff-per-file>

The git diff has the above structure,

  1. It would be nice to know which file diff is in hunk units
  2. I am trying to stage / unstage by clicking the header part, so I am happy if it is wider

For that reason, it displays the diff header with each hunk.

<diff-per-file>
	<diff-header />
	<hunk>
		<hunk-header />
		<hunk-body />
	</hunk>
	<diff-header />
	<hunk>
		<hunk-header />
	    <hunk-body />
	</hunk>
</diff-per-file>

<diff-per-file>
	<diff-header />
	<hunk>
		<hunk-header dummy="new file" />
		<hunk-body img="icon.png " />
	</hunk>
</diff-per-file>

If you combine multiple hunks of the same diff, the diff headers will be duplicated, There seems to be no problem even if they are output to the patch all at once. (However, I basically only do git apply on a hunk basis, so there may be hidden issues)

hunk header

Therefore, multiple lines of text are displayed on one line of the list. JLabel etc. can put html (like?) Together with <html> </ html>.

Collector<CharSequence, ?, String> toHtml = Collectors.joining(
            "</nobr><br><nobr>",
            "<html><nobr>",
            "</nobr></html>");
Arrays.stream(String of the header part.split("\n")).collect(toHtml)

I'm wondering if <nobr> </ nobr> can be used in any environment now or in the future, but it's useful.

hunk body

The output of git diff is as it is. It is easy to understand if the colors are different for the add line, delete line, and context line.

Asynchronous acquisition of images

When adding or deleting images (such as binary files), it is handled in file units, so The unit hunk doesn't exist and doesn't appear in the output of git diff.

However, I want to display the image because it is a big deal. In the previous issue, we mentioned the method and precautions.

When displaying in a list

With that in mind, there are two display methods.

I'm not happy to wait, so let's display what can be displayed first.

(I didn't think it would be a hassle to implement, so I chose the latter as a matter of course, but I think it's totally ant to wait because it depends on the performance of the local machine, not via the network.)

Update list display line by line

However, JList does not seem to be able to update row by row. So we use JTable with only one column.

As a result of various trials, I divided it into a line with only text and a line with only images. Updating in the following order gave the desired performance.

  1. Determine the height of the text line first
  2. Start asynchronous acquisition of the image in the image row
  3. Once you have the image, determine the height of the line from the image

The following includes hacks that depend on Swing's internal implementation, so Depending on the version, the same idea and the same code may not work. I'm checking javac 1.8.0_144. (Do you understand that it is the same as the javac version?) (By the way, are there any plans to dramatically improve the existing components of Swing in the future?)

Determine the height of the text line first

When it is necessary to display / update each row in JTable, the TableCellRenderer set for each column is called. TableCellRenderer is an interface.

public interface TableCellRenderer {

	Component getTableCellRendererComponent(JTable table, 
	                                        Object value,
	                                        boolean isSelected, 
	                                        boolean hasFocus,
	                                        int row, 
	                                        int column);
}

There should be several ways to update each row, Here we use JTable # setRowHeight (int row, int rowHeight). It is essential to change the height from row to row.

You can find the height of the text by putting the text in JLabel. Create a JLabel and set the height before the TableCellRenderer is called.

The height of each row is updated when the data associated with the table changes. This is a sample code.

//I will put together the setRowHeight of the text row first
preRender = () -> {
    //If you don't do one of the following, the maximum number of rows in the table will depend on the number of rows initially displayed.
    //Probably rowModel inside JTable= null;I think you need
    this.table.setRowHeight(1);
//    setRowSorter(null);
//    tableChanged(new TableModelEvent(getModel()));
    final int size = dataModel.getSize();
    for (int i = 0; i < size; i++) {
        final ListItem item = dataModel.getElementAt(i);
        final ImageHolder imageHolder = item.imageHolder();
        //If not an image line
        if (imageHolder == null || !imageHolder.hasLoader()) {
            //Don't cache because it shouldn't be a big cost
            final JLabel label = new JLabel();
            this.renderer.render(label, item, false, null);
            final int height = label.getPreferredSize().height;
            table.setRowHeight(i, height);
        }
    }
};

I wrote it in the sample code, but as a caveat,

If you don't call, etc., the maximum number of rows in the table will depend on the number of rows initially displayed. If you initially display 3 rows, the table can only display 3 rows. I'm not sure why this happens, but Initializing the JTable private SizeSequence rowModel; seems to behave as expected.

Do not call JTable # setRowHeight (int row, int rowHeight) more than once for the same row, It may be that.

Start asynchronous acquisition of images in image rows

As with the text, you can start in advance, I didn't feel any performance problems, so Use TableCellRenderer # getTableCellRendererComponent.

In any case, TableCellRenderer # getTableCellRendererComponent will be called at least when updating the surrounding rows, so It must be implemented so that the image acquisition and the callback call after acquisition are made only once.

Once you have an image, determine the height of that row from the image

Since the display cannot be changed outside of TableCellRenderer # getTableCellRendererComponent, After getting the image, give the linked item the image Call JTable # setRowHeight (int row, int rowHeight). Then TableCellRenderer # getTableCellRendererComponent will be called again.

final TableColumn tableColumn = this.table.getColumnModel().getColumn(0);
tableColumn.setCellRenderer((table, value, isSelected, hasFocus, row, column) -> {
        final ListItem item = (ListItem) value;
        final JLabel label = new JLabel();
        //Start image acquisition here or set an image
        this.renderer.render(label, item, isSelected, imageIcon -> {
                //After image acquisition
                final int iconHeight = imageIcon.getIconHeight();
                table.setRowHeight(row, iconHeight);
        });
        return label;
});

progress

It's a progress gif that is familiar every time and has nothing to do with posting.

進捗_3.gif

There are still bugs and issues, but you can commit. I want to be able to display the history soon.

Afterword

Next time preview

Once you start writing

I was getting tired for some reason, so I decided to move on to the next issue.

Recent situation

I started writing this series and apps It was because I retired and enjoyed being unemployed.

I didn't intend to work for a while yet, Work came down from the sky, so I became a freelancer. It was a flow that I became a programmer, so I think it will continue as it is.

The latest is the renovation of the iOS app. I haven't written any code yet, but you should git commit in your own application. (I haven't run it on a mac yet, but I believe in the JVM)

If you have work at hand, you will be more motivated to make tools outside of work.

Swing

There is no particular reason to write in Swing, There was only "I don't want to write CSS", I intended to implement it temporarily for design, There are no particular problems, so I plan to continue as it is.

I wonder what kind of shape should be used for distribution.

Recommended Posts

Create a weekly git GUI client [5] First desktop app
Make a weekly git GUI client [4] diff et al.
Make a weekly git GUI client [2] stage / unstage incomplete capture
Create a new app in Rails
Try to create a server-client app
[Rails6] Create a new app with Rails [Beginner]
Create a GUI JSON Viewer with Ruby/GTK3
Create a TODO app in Java 7 Create Header
[Rails 5] Create a new app with Rails [Beginner]
Create a simple search app with Spring Boot
Let's create a RESTful email sending service + client
I tried to create a LINE clone app
Now, git GUI client self-made is very popular!