3. public class SearchForm extends Form {
private boolean searchPeople;
private String lastSearchValue;
private UITimer pendingTimer;
private long lastSearchTime;
private TextField searchField = new TextField();
private InfiniteContainer ic = new InfiniteContainer() {
@Override
public Component[] fetchComponents(int index, int amount) {
if(searchField.getText().length() < 2) {
return null;
}
int page = index / amount;
if(index % amount > 0) {
page++;
}
List<Component> response = new ArrayList<>();
if(searchPeople) {
List<User> results = ServerAPI.searchPeople(
searchField.getText(), page, amount);
if(results == null) {
return null;
SearchForm
Lets dive into the code starting with the fields and general types…
This is set to true when we are in people search mode
4. public class SearchForm extends Form {
private boolean searchPeople;
private String lastSearchValue;
private UITimer pendingTimer;
private long lastSearchTime;
private TextField searchField = new TextField();
private InfiniteContainer ic = new InfiniteContainer() {
@Override
public Component[] fetchComponents(int index, int amount) {
if(searchField.getText().length() < 2) {
return null;
}
int page = index / amount;
if(index % amount > 0) {
page++;
}
List<Component> response = new ArrayList<>();
if(searchPeople) {
List<User> results = ServerAPI.searchPeople(
searchField.getText(), page, amount);
if(results == null) {
return null;
SearchForm
We use this flag to detect if the search text actually changed
5. public class SearchForm extends Form {
private boolean searchPeople;
private String lastSearchValue;
private UITimer pendingTimer;
private long lastSearchTime;
private TextField searchField = new TextField();
private InfiniteContainer ic = new InfiniteContainer() {
@Override
public Component[] fetchComponents(int index, int amount) {
if(searchField.getText().length() < 2) {
return null;
}
int page = index / amount;
if(index % amount > 0) {
page++;
}
List<Component> response = new ArrayList<>();
if(searchPeople) {
List<User> results = ServerAPI.searchPeople(
searchField.getText(), page, amount);
if(results == null) {
return null;
SearchForm
This timer waits to make sure the user isn't still typing, so we won't send too many search queries at once
6. public class SearchForm extends Form {
private boolean searchPeople;
private String lastSearchValue;
private UITimer pendingTimer;
private long lastSearchTime;
private TextField searchField = new TextField();
private InfiniteContainer ic = new InfiniteContainer() {
@Override
public Component[] fetchComponents(int index, int amount) {
if(searchField.getText().length() < 2) {
return null;
}
int page = index / amount;
if(index % amount > 0) {
page++;
}
List<Component> response = new ArrayList<>();
if(searchPeople) {
List<User> results = ServerAPI.searchPeople(
searchField.getText(), page, amount);
if(results == null) {
return null;
SearchForm
The time of the last search helps us decide how long we should wait for the upcoming search query so we don't send too many queries
7. public class SearchForm extends Form {
private boolean searchPeople;
private String lastSearchValue;
private UITimer pendingTimer;
private long lastSearchTime;
private TextField searchField = new TextField();
private InfiniteContainer ic = new InfiniteContainer() {
@Override
public Component[] fetchComponents(int index, int amount) {
if(searchField.getText().length() < 2) {
return null;
}
int page = index / amount;
if(index % amount > 0) {
page++;
}
List<Component> response = new ArrayList<>();
if(searchPeople) {
List<User> results = ServerAPI.searchPeople(
searchField.getText(), page, amount);
if(results == null) {
return null;
SearchForm
This is the search text field where the user can type
8. public class SearchForm extends Form {
private boolean searchPeople;
private String lastSearchValue;
private UITimer pendingTimer;
private long lastSearchTime;
private TextField searchField = new TextField();
private InfiniteContainer ic = new InfiniteContainer() {
@Override
public Component[] fetchComponents(int index, int amount) {
if(searchField.getText().length() < 2) {
return null;
}
int page = index / amount;
if(index % amount > 0) {
page++;
}
List<Component> response = new ArrayList<>();
if(searchPeople) {
List<User> results = ServerAPI.searchPeople(
searchField.getText(), page, amount);
if(results == null) {
return null;
SearchForm
Search results add up into the InfiniteContainer for paging/refresh capabilities
9. public class SearchForm extends Form {
private boolean searchPeople;
private String lastSearchValue;
private UITimer pendingTimer;
private long lastSearchTime;
private TextField searchField = new TextField();
private InfiniteContainer ic = new InfiniteContainer() {
@Override
public Component[] fetchComponents(int index, int amount) {
if(searchField.getText().length() < 2) {
return null;
}
int page = index / amount;
if(index % amount > 0) {
page++;
}
List<Component> response = new ArrayList<>();
if(searchPeople) {
List<User> results = ServerAPI.searchPeople(
searchField.getText(), page, amount);
if(results == null) {
return null;
SearchForm
If search text is less than 2 characters we shouldn't start searching
10. public class SearchForm extends Form {
private boolean searchPeople;
private String lastSearchValue;
private UITimer pendingTimer;
private long lastSearchTime;
private TextField searchField = new TextField();
private InfiniteContainer ic = new InfiniteContainer() {
@Override
public Component[] fetchComponents(int index, int amount) {
if(searchField.getText().length() < 2) {
return null;
}
int page = index / amount;
if(index % amount > 0) {
page++;
}
List<Component> response = new ArrayList<>();
if(searchPeople) {
List<User> results = ServerAPI.searchPeople(
searchField.getText(), page, amount);
if(results == null) {
return null;
SearchForm
This is the standard page calculation logic that we have in other InfiniteContainer classes
11. }
int page = index / amount;
if(index % amount > 0) {
page++;
}
List<Component> response = new ArrayList<>();
if(searchPeople) {
List<User> results = ServerAPI.searchPeople(
searchField.getText(), page, amount);
if(results == null) {
return null;
}
for(User u : results) {
response.add(createEntry(u));
}
} else {
List<Post> results = ServerAPI.searchPosts(
searchField.getText(), page, amount);
if(results == null) {
return null;
}
for(Post u : results) {
response.add(createEntry(u));
}
}
SearchForm
We build results for people or posts in the same way, there are 2 createEntry methods that cover the logic for both cases
12. List<User> results = ServerAPI.searchPeople(
searchField.getText(), page, amount);
if(results == null) {
return null;
}
for(User u : results) {
response.add(createEntry(u));
}
} else {
List<Post> results = ServerAPI.searchPosts(
searchField.getText(), page, amount);
if(results == null) {
return null;
}
for(Post u : results) {
response.add(createEntry(u));
}
}
if(response.isEmpty()) {
return null;
}
return UIUtils.toArray(response);
}
};
SearchForm
The resulting components are returned on null in the case of an empty set. This is pretty direct and should be relatively obvious, the two big pieces that are obviously
missing from this code are the createEntry methods so lets get to them…
13. });
return;
}
}
lastSearchTime = System.currentTimeMillis();
ic.refresh();
}
}
private Component createEntry(User u) {
MultiButton mb = new MultiButton(u.fullName());
mb.setIcon(u.getAvatar(8));
mb.addActionListener(e -> new UserForm(u).show());
return mb;
}
private Component createEntry(Post p) {
MultiButton mb = new MultiButton(p.title.get());
mb.setTextLine2(p.content.get());
mb.setIcon(p.user.get().getAvatar(8));
mb.addActionListener(e -> new PostForm(p).show());
return mb;
}
}
SearchForm
The version that accepts the User object just creates a MultiButton for that. The same is true for the post entry
14. });
return;
}
}
lastSearchTime = System.currentTimeMillis();
ic.refresh();
}
}
private Component createEntry(User u) {
MultiButton mb = new MultiButton(u.fullName());
mb.setIcon(u.getAvatar(8));
mb.addActionListener(e -> new UserForm(u).show());
return mb;
}
private Component createEntry(Post p) {
MultiButton mb = new MultiButton(p.title.get());
mb.setTextLine2(p.content.get());
mb.setIcon(p.user.get().getAvatar(8));
mb.addActionListener(e -> new PostForm(p).show());
return mb;
}
}
SearchForm
I’ll get to the event handling code later, the UserForm & PostForm mentioned here don’t exist yet so I’ll discuss them after finishing the search class.
This is pretty trivial the next step isn't as simple…
15. return UIUtils.toArray(response);
}
};
public SearchForm() {
super(new BorderLayout());
searchField.setUIID("Title");
searchField.getAllStyles().setAlignment(LEFT);
searchField.addDataChangedListener((i, ii) -> updateSearch());
Toolbar tb = getToolbar();
tb.setTitleComponent(searchField);
Form previous = getCurrentForm();
tb.addMaterialCommandToLeftBar("", MATERIAL_CLOSE, e ->
previous.showBack());
tb.addMaterialCommandToRightBar("", MATERIAL_PERSON, e -> {
searchPeople = true;
ic.refresh();
});
tb.addMaterialCommandToRightBar("", MATERIAL_PAGES, e -> {
searchPeople = false;
ic.refresh();
});
add(CENTER, ic);
setEditOnShow(searchField);
}
SearchForm
We use BorderLayout so the InfiniteContainer can fit in the center, there is no other reason
16. return UIUtils.toArray(response);
}
};
public SearchForm() {
super(new BorderLayout());
searchField.setUIID("Title");
searchField.getAllStyles().setAlignment(LEFT);
searchField.addDataChangedListener((i, ii) -> updateSearch());
Toolbar tb = getToolbar();
tb.setTitleComponent(searchField);
Form previous = getCurrentForm();
tb.addMaterialCommandToLeftBar("", MATERIAL_CLOSE, e ->
previous.showBack());
tb.addMaterialCommandToRightBar("", MATERIAL_PERSON, e -> {
searchPeople = true;
ic.refresh();
});
tb.addMaterialCommandToRightBar("", MATERIAL_PAGES, e -> {
searchPeople = false;
ic.refresh();
});
add(CENTER, ic);
setEditOnShow(searchField);
}
SearchForm
The Title UIID makes the search field fit into the title area
17. return UIUtils.toArray(response);
}
};
public SearchForm() {
super(new BorderLayout());
searchField.setUIID("Title");
searchField.getAllStyles().setAlignment(LEFT);
searchField.addDataChangedListener((i, ii) -> updateSearch());
Toolbar tb = getToolbar();
tb.setTitleComponent(searchField);
Form previous = getCurrentForm();
tb.addMaterialCommandToLeftBar("", MATERIAL_CLOSE, e ->
previous.showBack());
tb.addMaterialCommandToRightBar("", MATERIAL_PERSON, e -> {
searchPeople = true;
ic.refresh();
});
tb.addMaterialCommandToRightBar("", MATERIAL_PAGES, e -> {
searchPeople = false;
ic.refresh();
});
add(CENTER, ic);
setEditOnShow(searchField);
}
SearchForm
In iOS the title is centered by default and that doesn't look good while editing so we left align explicitly for this case
18. return UIUtils.toArray(response);
}
};
public SearchForm() {
super(new BorderLayout());
searchField.setUIID("Title");
searchField.getAllStyles().setAlignment(LEFT);
searchField.addDataChangedListener((i, ii) -> updateSearch());
Toolbar tb = getToolbar();
tb.setTitleComponent(searchField);
Form previous = getCurrentForm();
tb.addMaterialCommandToLeftBar("", MATERIAL_CLOSE, e ->
previous.showBack());
tb.addMaterialCommandToRightBar("", MATERIAL_PERSON, e -> {
searchPeople = true;
ic.refresh();
});
tb.addMaterialCommandToRightBar("", MATERIAL_PAGES, e -> {
searchPeople = false;
ic.refresh();
});
add(CENTER, ic);
setEditOnShow(searchField);
}
SearchForm
This is the search operation, with every change to the text field we call the updateSearch method which performs the actual search
19. return UIUtils.toArray(response);
}
};
public SearchForm() {
super(new BorderLayout());
searchField.setUIID("Title");
searchField.getAllStyles().setAlignment(LEFT);
searchField.addDataChangedListener((i, ii) -> updateSearch());
Toolbar tb = getToolbar();
tb.setTitleComponent(searchField);
Form previous = getCurrentForm();
tb.addMaterialCommandToLeftBar("", MATERIAL_CLOSE, e ->
previous.showBack());
tb.addMaterialCommandToRightBar("", MATERIAL_PERSON, e -> {
searchPeople = true;
ic.refresh();
});
tb.addMaterialCommandToRightBar("", MATERIAL_PAGES, e -> {
searchPeople = false;
ic.refresh();
});
add(CENTER, ic);
setEditOnShow(searchField);
}
SearchForm
setTitleComponent places the component in the title area instead of the label that's there by default
20. return UIUtils.toArray(response);
}
};
public SearchForm() {
super(new BorderLayout());
searchField.setUIID("Title");
searchField.getAllStyles().setAlignment(LEFT);
searchField.addDataChangedListener((i, ii) -> updateSearch());
Toolbar tb = getToolbar();
tb.setTitleComponent(searchField);
Form previous = getCurrentForm();
tb.addMaterialCommandToLeftBar("", MATERIAL_CLOSE, e ->
previous.showBack());
tb.addMaterialCommandToRightBar("", MATERIAL_PERSON, e -> {
searchPeople = true;
ic.refresh();
});
tb.addMaterialCommandToRightBar("", MATERIAL_PAGES, e -> {
searchPeople = false;
ic.refresh();
});
add(CENTER, ic);
setEditOnShow(searchField);
}
SearchForm
This closes the search form by going to the previous form
21. return UIUtils.toArray(response);
}
};
public SearchForm() {
super(new BorderLayout());
searchField.setUIID("Title");
searchField.getAllStyles().setAlignment(LEFT);
searchField.addDataChangedListener((i, ii) -> updateSearch());
Toolbar tb = getToolbar();
tb.setTitleComponent(searchField);
Form previous = getCurrentForm();
tb.addMaterialCommandToLeftBar("", MATERIAL_CLOSE, e ->
previous.showBack());
tb.addMaterialCommandToRightBar("", MATERIAL_PERSON, e -> {
searchPeople = true;
ic.refresh();
});
tb.addMaterialCommandToRightBar("", MATERIAL_PAGES, e -> {
searchPeople = false;
ic.refresh();
});
add(CENTER, ic);
setEditOnShow(searchField);
}
SearchForm
These are the two buttons in the title area that let us toggle people & post search modes. We invoke refresh when there is a change which triggers a call to
fetchComponents in the InfiniteContainer for the first page
22. return UIUtils.toArray(response);
}
};
public SearchForm() {
super(new BorderLayout());
searchField.setUIID("Title");
searchField.getAllStyles().setAlignment(LEFT);
searchField.addDataChangedListener((i, ii) -> updateSearch());
Toolbar tb = getToolbar();
tb.setTitleComponent(searchField);
Form previous = getCurrentForm();
tb.addMaterialCommandToLeftBar("", MATERIAL_CLOSE, e ->
previous.showBack());
tb.addMaterialCommandToRightBar("", MATERIAL_PERSON, e -> {
searchPeople = true;
ic.refresh();
});
tb.addMaterialCommandToRightBar("", MATERIAL_PAGES, e -> {
searchPeople = false;
ic.refresh();
});
add(CENTER, ic);
setEditOnShow(searchField);
}
SearchForm
When we show the form the search field will instantly launch into editing mode, this is important with a virtual keyboard so it won't just have focus it will actually be in edit
mode
23. add(CENTER, ic);
setEditOnShow(searchField);
}
private void updateSearch() {
String text = searchField.getText();
if(text.length() > 2) {
if(lastSearchValue != null) {
if(lastSearchValue.equalsIgnoreCase(text)) {
return;
}
if(pendingTimer != null) {
pendingTimer.cancel();
}
long t = System.currentTimeMillis();
if(t - lastSearchTime < 800) {
pendingTimer = UITimer.timer((int)(t - lastSearchTime),
false, this, () -> {
lastSearchTime = System.currentTimeMillis();
ic.refresh();
});
return;
}
}
lastSearchTime = System.currentTimeMillis();
ic.refresh();
SearchForm
This leads us to the updateSearch() method which performs the actual search logic
24. add(CENTER, ic);
setEditOnShow(searchField);
}
private void updateSearch() {
String text = searchField.getText();
if(text.length() > 2) {
if(lastSearchValue != null) {
if(lastSearchValue.equalsIgnoreCase(text)) {
return;
}
if(pendingTimer != null) {
pendingTimer.cancel();
}
long t = System.currentTimeMillis();
if(t - lastSearchTime < 800) {
pendingTimer = UITimer.timer((int)(t - lastSearchTime),
false, this, () -> {
lastSearchTime = System.currentTimeMillis();
ic.refresh();
});
return;
}
}
lastSearchTime = System.currentTimeMillis();
ic.refresh();
SearchForm
If the text didn't change we don't do anything
25. add(CENTER, ic);
setEditOnShow(searchField);
}
private void updateSearch() {
String text = searchField.getText();
if(text.length() > 2) {
if(lastSearchValue != null) {
if(lastSearchValue.equalsIgnoreCase(text)) {
return;
}
if(pendingTimer != null) {
pendingTimer.cancel();
}
long t = System.currentTimeMillis();
if(t - lastSearchTime < 800) {
pendingTimer = UITimer.timer((int)(t - lastSearchTime),
false, this, () -> {
lastSearchTime = System.currentTimeMillis();
ic.refresh();
});
return;
}
}
lastSearchTime = System.currentTimeMillis();
ic.refresh();
SearchForm
If we have a search pending we kill that search request, since this all runs on the EDT this is a search that didn't start yet
26. add(CENTER, ic);
setEditOnShow(searchField);
}
private void updateSearch() {
String text = searchField.getText();
if(text.length() > 2) {
if(lastSearchValue != null) {
if(lastSearchValue.equalsIgnoreCase(text)) {
return;
}
if(pendingTimer != null) {
pendingTimer.cancel();
}
long t = System.currentTimeMillis();
if(t - lastSearchTime < 800) {
pendingTimer = UITimer.timer((int)(t - lastSearchTime),
false, this, () -> {
lastSearchTime = System.currentTimeMillis();
ic.refresh();
});
return;
}
}
lastSearchTime = System.currentTimeMillis();
ic.refresh();
SearchForm
If we already started a search we make sure it wasn't too soon, otherwise we'll postpone this search for later
27. add(CENTER, ic);
setEditOnShow(searchField);
}
private void updateSearch() {
String text = searchField.getText();
if(text.length() > 2) {
if(lastSearchValue != null) {
if(lastSearchValue.equalsIgnoreCase(text)) {
return;
}
if(pendingTimer != null) {
pendingTimer.cancel();
}
long t = System.currentTimeMillis();
if(t - lastSearchTime < 800) {
pendingTimer = UITimer.timer((int)(t - lastSearchTime),
false, this, () -> {
lastSearchTime = System.currentTimeMillis();
ic.refresh();
});
return;
}
}
lastSearchTime = System.currentTimeMillis();
ic.refresh();
SearchForm
When the timer elapses we update the search time and refresh, notice that since this is a UITimer the elapsing happens on the EDT
28. String text = searchField.getText();
if(text.length() > 2) {
if(lastSearchValue != null) {
if(lastSearchValue.equalsIgnoreCase(text)) {
return;
}
if(pendingTimer != null) {
pendingTimer.cancel();
}
long t = System.currentTimeMillis();
if(t - lastSearchTime < 800) {
pendingTimer = UITimer.timer((int)(t - lastSearchTime),
false, this, () -> {
lastSearchTime = System.currentTimeMillis();
ic.refresh();
});
return;
}
}
lastSearchTime = System.currentTimeMillis();
ic.refresh();
}
}
private Component createEntry(User u) {
MultiButton mb = new MultiButton(u.fullName());
SearchForm
If there is nothing pending we just do the search right now and refresh the InfiniteContainer. With this the search form should work and show search results instantly!
29. mainUI.addTab("", MATERIAL_WEB, 5f, new NewsfeedContainer());
FloatingActionButton fab =
FloatingActionButton.createFAB(MATERIAL_IMPORT_CONTACTS);
Container friends = fab.bindFabToContainer(new FriendsContainer());
fab.addActionListener(e -> uploadContacts());
mainUI.addTab("", MATERIAL_PEOPLE_OUTLINE, 5f,
friends);
mainUI.addTab("", MATERIAL_NOTIFICATIONS_NONE,
5f, new NotificationsContainer());
mainUI.addTab("", MATERIAL_MENU, 5f,
new MoreContainer());
add(CENTER, mainUI);
getToolbar().addMaterialCommandToLeftBar("",
MATERIAL_CAMERA_ALT, 4, e -> {});
getToolbar().addMaterialCommandToRightBar("",
MATERIAL_CHAT, 4, e -> {});
Button searchButton = new Button("Search", "TitleSearch");
setMaterialIcon(searchButton, MATERIAL_SEARCH);
getToolbar().setTitleComponent(searchButton);
searchButton.addActionListener(e -> new SearchForm().show());
}
private void uploadContacts() {
MainForm
The one last piece of the puzzle is plugging it into the UI which is actually trivial... We add this line to the MainForm constructor. With that you can now click the search
button and it will actually work with the data from the database.