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:
Add the package name and change the main class name to CodeVaultApp
.
Press Next
and select a valid JDK 8:
Press Next
and add the project name, and modify the location if required.
Press Next
, and finally import the gradle project. Review the settings and click Ok
.
The project will be created and opened. The main class CodeVaultApp
is shown.
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:
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
.
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:
@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:
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:
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:
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:
public class GitBranch extends GitRef {
public GitBranch(Ref ref) {
super(ref);
}
}
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();
}
}
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:
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:
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:
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.
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:
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:
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.
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:
@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);
}
}
}
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.
.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;
}
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.