The CodeVault App is a Gluon code sample. For a full list of Gluon code samples, refer to the Gluon website.

In this tutorial, we’ll explain how to create the CodeVault application using Gluon Desktop framework: A Git repo browser to demonstrate best practices using the framework with intentionally minimal functionality.

Before you start, be sure that you have installed the Gluon plugin for your IDE.

Note: This tutorial will use the plugin for IntelliJ, but it works as well on NetBeans and Eclipse.

Code: The code for this project can be found in the samples repository at GitHub. The sample is located under the directory code-vault. The reader can clone this repository or create the entire project from scratch, based on the following steps.

Creating the Project

Let’s create a new project using the Gluon plugin. Open IntelliJ and click Create New Project…​ and select Gluon on the left. Select Gluon Desktop - Single View Project from the list of available Projects:

Plugins Window

Add the package name and change the main class name to CodeVaultApp.

Package and Main

Press Next and select a valid JDK 8:

JDK8

Press Next and add the project name, and modify the location if required.

Name and Location

Press Next, and finally import the gradle project. Review the settings and click Ok.

Gradle Import

The project will be created and opened. The main class CodeVaultApp is shown.

CodeVaultApp class

Modifying the project

Let’s start modifying the default project to create our CodeVaultApp application.

Octicons font

Octicons is a font with GitHub icons. Download the latest release from here and add the file octicons-local.ttf to src/main/resources/com/gluonhq/codevault.

Now we’ll add an Utils package to load this font. Let’s create Octicons clase, as a subclass of org.controlsfx.glyphfont.GlyphFont.

It will load the font and it will register a bunch of org.controlsfx.glyphfont.INamedCharacter defined in the Glyph enum:

Octicons.java
public class Octicons extends GlyphFont {

    public static final String NAME = "github-octicons";

    public Octicons() {
        super(NAME, 16, CodeVaultApp.class.getResourceAsStream("octicons-local.ttf"));
        registerAll(Arrays.asList(Glyph.values()));
    }

    public static enum Glyph implements INamedCharacter {
        BRANCH('\uf020'),
        COMMIT('\uf01f'),
        CLONE('\uf04c'),
        TAG('\uf015'),
        FOLDER('\uf016'),
        REPO('\uf001'),
        SIGN_OUT('\uf032'),
        MARK_GITHUB('\uf00a');

        private final Character ch;

        Glyph( Character ch ) {
            this.ch = ch;
        }

        public char getChar() {
            return ch;
        }
    };
}

We can register the font on our main class, where we’ll change the application’s title to CodeVault, and rename the signIn action to openRepo.

CodeVaultApp.java
public class CodeVaultApp extends ParticleApplication {

    static {
        GlyphFontRegistry.register(new Octicons());
    }

    public CodeVaultApp() {
        super("CodeVault");
    }

    @Override
    public void postInit(Scene scene) {
        scene.getStylesheets().add(CodeVaultApp.class.getResource("style.css").toExternalForm());

        setTitle("CodeVault");

        getParticle().buildMenu("File -> [openRepo,---, exit]", "Help -> [about]");

        getParticle().getToolBarActions().addAll(actions("openRepo"));
    }
}

And now we modify the actions, adding the github-opticons or FontAwesome (this font is already registered by ControlsFX) icons:

MenuActions.java
@ParticleActions
public class MenuActions {

    @Inject
    ParticleApplication app;

    @ActionProxy(text="Exit",
                 graphic="font>github-octicons|SIGN_OUT",
                 accelerator="alt+F4")
    private void exit() {
        app.exit();
    }

    @ActionProxy(text="About",
            graphic="font>github-octicons|MARK_GITHUB",
            accelerator="ctrl+A")
    private void about() {
        Alert alert = new Alert(AlertType.INFORMATION);
        alert.setTitle("CodeVault");
        alert.setHeaderText("About CodeVault");
        alert.setGraphic(new ImageView(new Image(MenuActions.class.getResource("/icon.png").toExternalForm(), 48, 48, true, true)));
        alert.setContentText("This is a Gluon Desktop Application that creates a simple Git Repository");
        alert.showAndWait();
    }

    @ActionProxy(
            text="Open Repository",
            graphic="font>FontAwesome|FOLDER_OPEN",
            accelerator="ctrl+O")
    private void openRepo() {

    }
}

Open the Gradle window, and double click on Tasks→application→run to check what we have so far:

Running CodeVaultApp

The Git model

JGit is an EDL (new-style BSD) licensed, lightweight, pure Java library implementing the Git version control system: repository access routines, network protocols and core version control algorithms.

In order to use it in our application, we need to import this dependency:

build.gradle
dependencies {
    ...
    compile 'org.eclipse.jgit:org.eclipse.jgit:4.2.0.201601211800-r'
}

Let’s start by adding a git package. It will contain a GitRef class:

GitRef.java
public class GitRef {

    protected Ref ref;

    public GitRef(Ref ref) {
        this.ref = ref;
    }

    public String getId() {
        return ref.getObjectId().getName();
    }

    public String getFullName() {
        return ref.getName();
    }

    public String getShortName() {
        String[] path = getFullName().split("/");
        return path.length == 0? getFullName(): path[path.length-1];
    }

    @Override
    public String toString() {
        return getShortName();
    }
}

And GitBranch, GitTag and Topic subclasses:

GitBranch.java
public class GitBranch extends GitRef {

    public GitBranch(Ref ref) {
        super(ref);
    }
}
GitTag.java
public class GitTag extends GitRef {

    public GitTag(Ref ref) {
        super(ref);
    }

    @Override
    public String getId() {
        ObjectId peeledId = ref.getPeeledObjectId();
        return peeledId == null? super.getId(): peeledId.getName();
    }
}
Topic.java
public class Topic extends GitRef {

    private final String name;

    public Topic(String name) {
        super(null);
        this.name = name;
    }

    @Override
    public String getFullName() {
        return name;
    }

    @Override
    public String getShortName() {
        return name;
    }

    @Override
    public String getId() {
        return "";
    }
}

Our model will be the GitCommit class, based on a RevCommit object and a list of GitRef objects:

GitCommit.java
public class GitCommit {

    private RevCommit commit;
    private List<GitRef> refs;
    public GitCommit(RevCommit c, List<GitRef> refs ) {
        this.commit = c;
        this.refs = refs;
    }

    public List<GitRef> getRefs() {
        return refs;
    }

    public String getShortMessage() {
        return commit.getShortMessage();
    }

    public String getFullMessage() {
        return commit.getFullMessage();
    }

    public String getId() {
        return commit.getId().getName();
    }

    public String getHash() {
        return getId().substring(0,8);
    }

    public Date getTime() {
        return commit.getAuthorIdent().getWhen();
    }

    public String getAuthor() {
        PersonIdent personId = commit.getAuthorIdent();
        return String.format( "%s<%s>",  personId.getName(), personId.getEmailAddress());
    }
}

We can define now the GitRepository class, containing a Repository and a Git objects, and methods to access to its commits, branches and tags:

public class GitRepository {

    private static final String GIT_FOLDER_NAME = ".git";

    private Repository repo;
    private Git git;
    private final File location;

    public GitRepository(File location) throws GitRepoException {

        this.location = Objects.requireNonNull(location);

        if ( !isGitRepo(location)) {
            throw new GitRepoException("Git repository not found at " + location);
        }

        File gitDir =
                GIT_FOLDER_NAME.equals(location.getName()) ? location : new File(location, GIT_FOLDER_NAME);

        try {
            repo = new FileRepositoryBuilder()
                    .setGitDir(gitDir)
                    .readEnvironment() // scan environment GIT_* variables
                    .findGitDir()      // scan up the file system tree
                    .build();

            git = new Git(repo);

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        GitRepository that = (GitRepository) o;

        return location.equals(that.location);

    }

    public void close() {
        repo.close();
    }

    @Override
    public int hashCode() {
        return location.hashCode();
    }

    private boolean isGitRepo( File location ) {
        return (location.exists() && location.getName().endsWith(GIT_FOLDER_NAME)) ||
                (new File( location, GIT_FOLDER_NAME).exists());
    }

    public File getLocation() {
        return location;
    }

    public String getName() {
        return location.getName();
    }

    public Collection<GitCommit> getLog() {
        try {
            Collection<GitRef> refs = new HashSet<>(getBranches());
            refs.addAll(getTags());

            Map<String, List<GitRef>> refMap = refs
                    .stream()
                    .collect(Collectors.groupingBy(GitRef::getId));

            return StreamSupport
                    .stream(git.log().all().call().spliterator(), false)
                    .map( c -> new GitCommit(c, refMap.getOrDefault(c.getId().getName(), Collections.emptyList())))
                    .collect(Collectors.toList());
        } catch (GitAPIException | IOException e) {
            e.printStackTrace();
            return new ArrayList<>();
        }
    }

    public Collection<GitBranch> getBranches() {
        try {
            return StreamSupport
                    .stream(git.branchList().call().spliterator(), false)
                    .map(ref -> new GitBranch(ref))
                    .collect(Collectors.toSet());
        } catch (GitAPIException e) {
            e.printStackTrace();
            return new HashSet<>();
        }
    }

    public Collection<GitTag> getTags() {
        try {
            return StreamSupport
                    .stream(git.tagList().call().spliterator(), false)
                    .map(ref -> new GitTag(!ref.isPeeled() ? ref : repo.peel(ref)))
                    .collect(Collectors.toSet());
        } catch (GitAPIException e) {
            e.printStackTrace();
            return new HashSet<>();
        }
    }
}

where GitRepoException class is:

GitRepoException.java
public class GitRepoException extends RuntimeException {

    public GitRepoException(String message, Throwable cause) {
        super(message, cause);
    }

    public GitRepoException(String message) {
        super(message);
    }

    public GitRepoException(Throwable cause) {
        super(cause);
    }
}

For convenience, we’ll also create this utility class:

UITools.java
public class UITools {
    public static Node getIcon(Octicons.Glyph iconType ) {
        Glyph icon = GlyphFontRegistry.font(Octicons.NAME).create(iconType).size(14);
        icon.getStyleClass().add("ref-icon");
        return icon;
    }

    public static Node getRefIcon(GitRef ref) {
        if (ref instanceof GitTag) {
            return getIcon(Octicons.Glyph.TAG);
        } else if (ref instanceof GitBranch) {
            return getIcon(Octicons.Glyph.BRANCH);
        } else {
            return null;
        }
    }
}

Modifying the View

Let’s rename the view to RepoManagerView and remove the default content. We’ll add a TabPane as the main container, and each Tab we add to it will allow browsing a different repository.

Each tab will be created by loading an FXML. Create a new file named repolog.fxml under src/main/resources/com/gluonhq/codevault/view, and edit it with Scene Builder.

Set a SplitPane as the main container, and add a TreeView to the left and a TableView with four columns to the right.

RepolLog fxml

Creating the controller

Add RepoLogController as controller, copy and paste the skeleton sample controller code, setting the type of the table to GitCommit, while the tree’s type will be GitRef.

For the tree, this will be the custom tree cell:

InfoTreeCell.java
public class InfoTreeCell extends TreeCell<GitRef> {

    private final String lastStyle = "topic";

    {
        getStyleClass().add("repoViewCell");
    }

    @Override
    protected void updateItem(GitRef ref, boolean empty) {
        super.updateItem(ref, empty);
        if (ref == null || empty) {
            setText(null);
            setGraphic(null);
        } else {
            if (ref instanceof Topic) {
                if (!getStyleClass().contains(lastStyle)) {
                    getStyleClass().add(lastStyle);
                }
            } else {
                if (getStyleClass().contains(lastStyle)) {
                    getStyleClass().remove(lastStyle);
                }
            }

            setText(ref.getShortName());
            setGraphic(UITools.getRefIcon(ref));
        }
    }
}

And the description table column will use this table cell:

CommitDescriptionTableCell.java
public class CommitDescriptionTableCell extends TableCell<GitCommit, GitCommit> {

    private HBox labels = new HBox(1);

    @Override
    protected void updateItem(GitCommit commit, boolean empty) {
        super.updateItem(commit, empty);
        if (commit == null || empty ) {
            setText(null);
            setGraphic(null);
            setTooltip(null);
        } else {
            setText(commit.getShortMessage());
            setTooltip(new Tooltip(commit.getFullMessage()));
            labels.getChildren().setAll(
                    commit.getRefs().stream()
                            .map(this::makeRefLabel)
                            .collect(Collectors.toList())
            );
            setGraphic( labels.getChildren().isEmpty()? null: labels);
        }
    }

    private Label makeRefLabel(GitRef ref) {
        Label refLabel = new Label(ref.getShortName());
        refLabel.setGraphic(UITools.getRefIcon(ref));
        if (ref instanceof GitTag) {
            refLabel.getStyleClass().add("tag-ref");
        } else if (ref instanceof GitBranch) {
            refLabel.getStyleClass().add("branch-ref");
        } else {
            refLabel.getStyleClass().add("unknown-ref");
        }
        return refLabel;
    }
}

In the LogRepoController, we’ll add repository, an observable property that wraps a GitRepository object. Whenever this property changes, we’ll set the table items with the new repository list of commits and we’ll build the tree with the repository branches and tags.

RepoLogController.java
public class RepoLogController {

    @FXML
    private TreeView<GitRef> info;

    @FXML
    private TableView<GitCommit> table;

    @FXML
    private TableColumn<GitCommit, String> tcCommit;

    @FXML
    private TableColumn<GitCommit, GitCommit> tcDescription;

    @FXML
    private TableColumn<GitCommit, String> tcAuthor;

    @FXML
    private TableColumn<GitCommit, Date> tcDate;

    // repositoryProperty
    private final ObjectProperty<GitRepository> repository = new SimpleObjectProperty<>(this, "repository");

    public final ObjectProperty<GitRepository> repositoryProperty() {
        return repository;
    }

    public final GitRepository getRepository() {
        return repository.get();
    }

    public final void setRepository(GitRepository value) throws GitRepoException {
        repository.set(value);
    }

    public void initialize() {
        SplitPane.setResizableWithParent(info, false);

        info.setCellFactory(tree -> new InfoTreeCell());

        tcDate.setCellValueFactory( new PropertyValueFactory<>("time"));
        tcAuthor.setCellValueFactory( new PropertyValueFactory<>("author"));
        tcCommit.setCellValueFactory( new PropertyValueFactory<>("hash"));

        tcDescription.setCellValueFactory(param -> new ReadOnlyObjectWrapper<GitCommit>(param.getValue()));
        tcDescription.setCellFactory(column -> new CommitDescriptionTableCell());

        repositoryProperty().addListener((obs, ov, repo) -> {
            if (repo != null) {
                table.getItems().setAll(repo.getLog());
                info.setRoot(createRepoInfoModel());
            } else {
                table.getItems().clear();
                info.setRoot(null);
            }
        });

        info.getSelectionModel().selectedItemProperty().addListener((obs, ov, nv) -> {
            if (nv.isLeaf()) {
                table.getItems().stream()
                        .filter(commit -> commit.getRefs().size() > 0)
                        .filter(commit -> commit.getRefs().get(0).getShortName().equals(nv.getValue().getShortName()))
                        .findFirst().ifPresent(commit -> {
                            table.getSelectionModel().select(commit);
                            table.scrollTo(commit);
                        });
            }
        });
    }

    private TreeItem<GitRef> createRepoInfoModel() {

        GitRepository repo = getRepository();

        TreeItem<GitRef> branches = createTopicFromElements("Branches", repo.getBranches(), true);

        List<TreeItem<GitRef>> topics = Arrays.asList(
                branches,
                createTopicFromElements("Tags", repo.getTags(), false));

        return createTopicFromTreeItems(repo.getName(), topics, true);
    }

    private static TreeItem<GitRef> createTopicFromElements(String name,
            Collection<? extends GitRef> children,
            boolean expand) {
        return createTopicFromTreeItems(name,
                children.stream()
                        .map(c -> new TreeItem<>(c))
                        .sorted((o1, o2) -> o2.getValue().getShortName().compareTo(o1.getValue().getShortName()))
                        .collect(Collectors.toList()),
                expand);
    }

    private static TreeItem<GitRef> createTopicFromTreeItems(String name,
            Collection<TreeItem<GitRef>> children,
            Boolean expand) {
        TreeItem<GitRef> result = new TreeItem<>(new Topic(name));
        result.setExpanded(expand);
        result.getChildren().addAll(children);
        return result;
    }
}

Loading repositories

Now that everything is in place, we can load the repositories, if any, in our single view:

RepoManagerView.java
@ParticleView(name = "repoManager", isDefault = true)
public class RepoManagerView implements View {

    private static final Logger LOGGER = Logger.getLogger(RepoManagerView.class.getName());

    @Inject private StateManager stateManager;

    private final TabPane tabs = new TabPane();

    @Override
    public void init() {
        stateManager.setPersistenceMode(StateManager.PersistenceMode.USER);
        tabs.setSide(Side.BOTTOM);

        stateManager.getPropertyAsString("repoLocations").ifPresent(s -> {
            String locations = s.trim();
            if (locations.length() > 0) {
                for (String location : locations.split(",")) {
                    openRepo(new File(location));
                }
            }
        });
    }

    @Override
    public Node getContent() {
        return tabs;
    }

    public void openRepo(File location) {

        FXMLLoader fxmlLoader = new FXMLLoader();
        try {
            final GitRepository repo = new GitRepository(location);

            // if open already - select it
            Tab tab = isRepoOpen(repo);
            if (tab == null) {
                fxmlLoader.setRoot(null);
                fxmlLoader.setLocation(RepoLogController.class.getResource("repolog.fxml"));

                Node content = fxmlLoader.load();
                RepoLogController controller = fxmlLoader.getController();
                controller.setRepository(repo);

                tab = new Tab(repo.getName(), content);
                tab.setGraphic(getGitIcon());
                tab.setUserData(repo);
                tab.setTooltip(new Tooltip(repo.getLocation().toString()));

                tabs.getTabs().add(tab);
            }
            tabs.getSelectionModel().select(tab);

        } catch (IOException e) {
            LOGGER.log(Level.SEVERE, "Can not load FXML file '" + fxmlLoader.getLocation() + "'", e);
        } catch (GitRepoException e) {
            ExceptionDialog dlg = new ExceptionDialog(e);
            dlg.initOwner(tabs.getScene().getWindow());
            dlg.setHeaderText(null);
            dlg.showAndWait();
        }
    }

    private Tab isRepoOpen(GitRepository repo) {
        Objects.requireNonNull(repo);
        for(Tab tab : tabs.getTabs()) {
            if (repo.equals(tab.getUserData())) {
                return tab;
            }
        }
        return null;
    }

    private Glyph getGitIcon() {
        return GlyphFontRegistry.font("FontAwesome").create(FontAwesome.Glyph.GIT_SQUARE);
    }

    @Override
    public void dispose() {

        String locations = tabs.getTabs().stream()
                .map(tab -> {
                    GitRepository repo = (GitRepository) tab.getUserData();
                    try {
                        return repo.getLocation().toString();
                    } finally {
                        repo.close();
                    }
                })
                .collect(Collectors.joining(","));

        stateManager.getStateIO().setProperty("repoLocations", locations);
    }
}

We just need to add the code to the action menu:

@ParticleActions
public class MenuActions {

    @Inject private ViewManager viewManager;

    @ActionProxy(text="Open Repository",
            graphic="font>FontAwesome|FOLDER_OPEN",
            accelerator="ctrl+O")
    private void openRepo() {
        View currentView = viewManager.getCurrentView();

        if (currentView instanceof RepoManagerView) {
            RepoManagerView view = (RepoManagerView) currentView;
            DirectoryChooser dirChooser = new DirectoryChooser();
            dirChooser.setTitle("Open Git Repository");
            Optional.ofNullable(dirChooser.showDialog(app.getPrimaryStage())).ifPresent(view::openRepo);
        }
    }
}

Testing the application

Time for a test. If you have cloned a Git repo on your machine, you’ll be able to open it with the application.

Running the app

Notice that if you select any of the tags, it will select and scroll the affected row in the table.

Styling the application

Final step, adding some css to the application. Be aware that the basic.css has to be renamed to repomanager.css to follow the convention.

repomanager.css
.view {
    -new-black: #4D5052;

    -repoViewBackgroundColor: #4D5052;
    -repoViewSelectionBkgColor: #393939;
    -repoViewTextColor: #BFC7C7;
    -repoViewTopicColor: lightblue;
    -repoViewCurrentBranchColor: white;
}

.split-pane > .split-pane-divider {
    -fx-padding: 0 0 -2 0;
}

.ref-icon {
    -fx-text-fill: -new-black;
    -fx-padding: -10 -3 -5 0;
}

/*  Repository TableView */

.label.tag-ref {
    -fx-background-color: #fff3cc;
    -fx-border-color: -new-black;
    -fx-text-fill: -new-black;
    -fx-padding: 0 4 0 4;
    -fx-border-radius: 4;
    -fx-background-radius: 4;
}

.label.branch-ref {
    -fx-background-color: #b0f1ff;
    -fx-border-color: -new-black;
    -fx-text-fill: -new-black;
    -fx-padding: 0 4 0 4;
    -fx-border-radius: 4;
    -fx-background-radius: 4;
}

.label.unknown-ref {
    -fx-background-color: #e58aff;
    -fx-border-color: -new-black;
    -fx-text-fill: -new-black;
    -fx-padding: 0 4 0 4;
    -fx-border-radius: 4;
    -fx-background-radius: 4;
}

/* Repository Info TreeView*/

.repoViewTree   {
    -fx-font-size: 1.04em;
}

.repoViewCell,
.repoViewCell > .ref-icon {
    -fx-background-color: -repoViewBackgroundColor;
    -fx-text-fill: -repoViewTextColor;
    -fx-graphic-text-gap: 5;
}

.repoViewCell > .text,
.repoViewCell > .ref-icon > .text {
    -fx-effect: dropshadow( one-pass-box, black, 0,0,1,1 );
}

.repoViewCell:selected   {
    -fx-background-color: -repoViewSelectionBkgColor;
}

.repoViewCell:selected > .ref-icon {
    -fx-text-fill: -repoViewTextColor;
    -fx-background-color: -repoViewSelectionBkgColor;
}

.repoViewCell > .tree-disclosure-node > .arrow {
    -fx-background-color: -repoViewTextColor;
}

.repoViewCell.topic {
    -fx-text-fill: -repoViewTopicColor;
}

.repoViewCell.currentBranch  {
    -fx-text-fill: -repoViewCurrentBranchColor;
}
Running the styled app

Conclusion

Throughout this post we’ve covered in detail the basic steps to create a desktop application with the Gluon Desktop framework starting from a Single View project.

If you have made it this far, congratulations, we hope this sample and the documentation was helpful to you! In case you have any questions, your first stop should be the Gluon support page, where you can access the latest documentation, and the Gluon knowledge base. Gluon also recommends the community to support each other over at the Gluon StackOverflow page. Finally, Gluon offers commercial support as well, to kick start your projects.