Skip to content

Commit 2bd263d

Browse files
committed
PTBAS-738: replace TextFlow element in business logic with custom StyledMessage class
1 parent 8254c79 commit 2bd263d

3 files changed

Lines changed: 134 additions & 56 deletions

File tree

src/main/java/de/doubleslash/keeptime/controller/HeimatController.java

Lines changed: 28 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@
2626
import de.doubleslash.keeptime.rest.integration.heimat.model.HeimatTask;
2727
import de.doubleslash.keeptime.rest.integration.heimat.model.HeimatTime;
2828
import de.doubleslash.keeptime.view.ProjectReport;
29-
import javafx.scene.text.Text;
30-
import javafx.scene.text.TextFlow;
3129
import org.slf4j.Logger;
3230
import org.slf4j.LoggerFactory;
3331
import org.springframework.beans.factory.annotation.Autowired;
@@ -147,19 +145,20 @@ public List<Mapping> getTableRows(final LocalDate currentReportDate, final List<
147145
pr.appendToWorkNotes(currentWorkNote);
148146
}
149147
final String keeptimeNotes = pr.getNotes();
150-
TextFlow canBeSyncedMessage;
148+
StyledMessage canBeSyncedMessage;
151149

152150
if (!isMappedInHeimat) {
153-
canBeSyncedMessage = new TextFlow(new Text("Not mapped to Heimat task.\nMap in settings dialog."));
151+
canBeSyncedMessage = StyledMessage.of(
152+
new StyledMessage.TextSegment("Not mapped to Heimat task.\nMap in settings dialog."));
154153
} else if (heimatTasks.stream().noneMatch(ht -> ht.id() == optHeimatMapping.get().getExternalTaskId())) {
155-
canBeSyncedMessage = new TextFlow(new Text("Heimat Task is not available (anymore).\nPlease check mappings in settings dialog."));
154+
canBeSyncedMessage = StyledMessage.of(new StyledMessage.TextSegment(
155+
"Heimat Task is not available (anymore).\nPlease check mappings in settings dialog."));
156156
isMappedInHeimat = false;
157157
} else {
158158
final ExternalProjectMapping externalProjectMapping = optHeimatMapping.get();
159-
Text externalTaskName = new Text(externalProjectMapping.getExternalTaskName());
160-
externalTaskName.setStyle("-fx-font-weight: bold;");
161-
canBeSyncedMessage = new TextFlow(new Text("Sync to "), externalTaskName,
162-
new Text("\n(" + externalProjectMapping.getExternalProjectName() + ")"));
159+
canBeSyncedMessage = StyledMessage.of(new StyledMessage.TextSegment("Sync to "),
160+
new StyledMessage.TextSegment(externalProjectMapping.getExternalTaskName(), true),
161+
new StyledMessage.TextSegment("\n(" + externalProjectMapping.getExternalProjectName() + ")"));
163162
}
164163

165164
final String bookingHint = heimatTasks.stream()
@@ -168,7 +167,6 @@ public List<Mapping> getTableRows(final LocalDate currentReportDate, final List<
168167
.findAny()
169168
.orElseGet(String::new);
170169

171-
172170
if (optionalExistingMapping.isPresent()) {
173171
final Mapping existingMapping = optionalExistingMapping.get();
174172
final ArrayList<Project> projects = new ArrayList<>(existingMapping.projects());
@@ -178,18 +176,18 @@ public List<Mapping> getTableRows(final LocalDate currentReportDate, final List<
178176
final boolean shouldBeSynced =
179177
isMappedInHeimat && differenceGreaterOrEqual15Minutes(heimatSeconds, keepTimeSeconds);
180178
final Mapping mapping = new Mapping(isMappedInHeimat ? optHeimatMapping.get().getExternalTaskId() : -1,
181-
isMappedInHeimat, shouldBeSynced, canBeSyncedMessage, bookingHint, existingMapping.existingTimes(), projects,
182-
existingMapping.heimatNotes(), existingMapping.keeptimeNotes() + ". " + keeptimeNotes, heimatSeconds,
183-
keepTimeSeconds);
179+
isMappedInHeimat, shouldBeSynced, canBeSyncedMessage, bookingHint, existingMapping.existingTimes(),
180+
projects, existingMapping.heimatNotes(), existingMapping.keeptimeNotes() + ". " + keeptimeNotes,
181+
heimatSeconds, keepTimeSeconds);
184182
list.remove(existingMapping);
185183
list.add(mapping);
186184
} else {
187185
final boolean shouldBeSynced =
188186
isMappedInHeimat && differenceGreaterOrEqual15Minutes(heimatTimeSeconds, projectWorkSeconds);
189187
final List<Project> projects = Collections.singletonList(project);
190188
final Mapping mapping = new Mapping(isMappedInHeimat ? optHeimatMapping.get().getExternalTaskId() : -1,
191-
isMappedInHeimat, shouldBeSynced, canBeSyncedMessage, bookingHint, optionalAlreadyBookedTimes, projects,
192-
heimatNotes, keeptimeNotes, heimatTimeSeconds, projectWorkSeconds);
189+
isMappedInHeimat, shouldBeSynced, canBeSyncedMessage, bookingHint, optionalAlreadyBookedTimes,
190+
projects, heimatNotes, keeptimeNotes, heimatTimeSeconds, projectWorkSeconds);
193191
list.add(mapping);
194192
}
195193
}
@@ -209,8 +207,8 @@ public List<Mapping> getTableRows(final LocalDate currentReportDate, final List<
209207
.filter(t -> t.id() == times.get(0).taskId())
210208
.findAny()
211209
.orElseThrow();
212-
final Mapping mapping = new Mapping(id, true, false,
213-
new TextFlow(new Text("Not mapped in KeepTime\n\n" + heimatTask.name() + "\n" + heimatTask.taskHolderName())), "", times,
210+
final Mapping mapping = new Mapping(id, true, false, StyledMessage.of(new StyledMessage.TextSegment(
211+
"Not mapped in KeepTime\n\n" + heimatTask.name() + "\n" + heimatTask.taskHolderName())), "", times,
214212
new ArrayList<>(0), heimatNotes, "", heimatTimeSeconds, 0);
215213
list.add(mapping);
216214
});
@@ -233,18 +231,17 @@ public List<Mapping> getTableRows(final LocalDate currentReportDate, final List<
233231
String heimatNotes = addHeimatNotes(times);
234232
long heimatTimeSeconds = addHeimatTimes(times);
235233

236-
Text externalTaskName = new Text(externalProjectMapping.getExternalTaskName());
237-
externalTaskName.setStyle("-fx-font-weight: bold;");
238-
TextFlow syncMessage = new TextFlow(new Text("Present in HEIMAT but not KeepTime\n\nSync to "), externalTaskName,
239-
new Text("\n(" + externalProjectMapping.getExternalProjectName() + ")"));
240-
241-
final Mapping mapping2 = new Mapping(id, true, false,
242-
syncMessage, "", times, mappedProjects.stream()
243-
.filter(
244-
mp -> mp.getExternalTaskId()
245-
== id)
246-
.map(ExternalProjectMapping::getProject)
247-
.toList(),
234+
StyledMessage syncMessage = StyledMessage.of(
235+
new StyledMessage.TextSegment("Present in HEIMAT but not KeepTime\n\nSync to "),
236+
new StyledMessage.TextSegment(externalProjectMapping.getExternalTaskName(), true),
237+
new StyledMessage.TextSegment("\n(" + externalProjectMapping.getExternalProjectName() + ")"));
238+
239+
final Mapping mapping2 = new Mapping(id, true, false, syncMessage, "", times, mappedProjects.stream()
240+
.filter(
241+
mp -> mp.getExternalTaskId()
242+
== id)
243+
.map(ExternalProjectMapping::getProject)
244+
.toList(),
248245
heimatNotes, "", heimatTimeSeconds, 0);
249246
list.add(mapping2);
250247
});
@@ -437,8 +434,8 @@ public ExistingAndInvalidMappings getExistingProjectMappings(List<HeimatTask> ex
437434

438435
public record UserMapping(Mapping mapping, boolean shouldSync, String userNotes, int userMinutes) {}
439436

440-
public record Mapping(long heimatTaskId, boolean canBeSynced, boolean shouldBeSynced, TextFlow syncMessage, String bookingHint,
441-
List<HeimatTime> existingTimes, List<Project> projects, String heimatNotes,
437+
public record Mapping(long heimatTaskId, boolean canBeSynced, boolean shouldBeSynced, StyledMessage syncMessage,
438+
String bookingHint, List<HeimatTime> existingTimes, List<Project> projects, String heimatNotes,
442439
String keeptimeNotes, long heimatSeconds, long keeptimeSeconds) {}
443440

444441
public record HeimatErrors(UserMapping mapping, String errorMessage) {}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright 2025 doubleSlash Net Business GmbH
2+
//
3+
// This file is part of KeepTime.
4+
// KeepTime is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU General Public License
15+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package de.doubleslash.keeptime.model;
18+
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
22+
/**
23+
* Represents a styled text message composed of multiple text segments. This class provides a UI-agnostic way to
24+
* represent formatted text, allowing separation of business logic from UI components.
25+
*/
26+
public class StyledMessage {
27+
28+
/**
29+
* Represents a single text segment with optional styling.
30+
*
31+
* @param text
32+
* The text content
33+
* @param bold
34+
* Whether the text should be displayed in bold
35+
*/
36+
public record TextSegment(String text, boolean bold) {
37+
public TextSegment(String text) {
38+
this(text, false);
39+
}
40+
}
41+
42+
private final List<TextSegment> segments;
43+
44+
public StyledMessage(List<TextSegment> segments) {
45+
this.segments = new ArrayList<>(segments);
46+
}
47+
48+
/**
49+
* Creates a StyledMessage from a variable number of text segments.
50+
*
51+
* @param segments
52+
* The text segments to include in the message
53+
* @return A new StyledMessage containing the provided segments
54+
*/
55+
public static StyledMessage of(TextSegment... segments) {
56+
return new StyledMessage(List.of(segments));
57+
}
58+
59+
public List<TextSegment> getSegments() {
60+
return new ArrayList<>(segments);
61+
}
62+
63+
/**
64+
* Returns the message as plain text without styling.
65+
*/
66+
public String toPlainText() {
67+
return segments.stream().map(TextSegment::text).reduce("", String::concat);
68+
}
69+
}
70+

src/main/java/de/doubleslash/keeptime/view/ExternalProjectsSyncController.java

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import de.doubleslash.keeptime.common.SvgNodeProvider;
2323
import de.doubleslash.keeptime.controller.HeimatController;
2424
import de.doubleslash.keeptime.model.Project;
25+
import de.doubleslash.keeptime.model.StyledMessage;
2526
import de.doubleslash.keeptime.model.Work;
2627
import de.doubleslash.keeptime.rest.integration.heimat.model.HeimatTask;
2728
import de.doubleslash.keeptime.viewpopup.SearchPopup;
@@ -208,29 +209,22 @@ public void initForDate(LocalDate currentReportDate, List<Work> currentWorkItems
208209
});
209210

210211
heimatTaskSearchPopup = new SearchPopup<>(tasksNotInList);
211-
heimatTaskSearchPopup.setDisplayTextFunction(
212-
task -> task.taskHolderName() + " - " + task.name()
213-
);
212+
heimatTaskSearchPopup.setDisplayTextFunction(task -> task.taskHolderName() + " - " + task.name());
214213

215214
heimatTaskSearchPopup.setOnItemSelected((selectedTask, popup) -> {
216-
if (selectedTask == null) return;
217-
boolean alreadyExists = items.stream()
218-
.anyMatch(row -> row.mapping.heimatTaskId() == selectedTask.id());
219-
if (alreadyExists) return;
215+
if (selectedTask == null)
216+
return;
217+
boolean alreadyExists = items.stream().anyMatch(row -> row.mapping.heimatTaskId() == selectedTask.id());
218+
if (alreadyExists)
219+
return;
220220

221-
Text externalTaskName = new Text(selectedTask.name());
222-
externalTaskName.setStyle("-fx-font-weight: bold;");
223-
TextFlow syncMessage = new TextFlow(new Text("Manually added\n\nSync to "), externalTaskName,
224-
new Text("\n(" + selectedTask.taskHolderName() + ")"));
221+
StyledMessage syncMessage = StyledMessage.of(new StyledMessage.TextSegment("Manually added\n\nSync to "),
222+
new StyledMessage.TextSegment(selectedTask.name(), true),
223+
new StyledMessage.TextSegment("\n(" + selectedTask.taskHolderName() + ")"));
225224

226225
TableRow addedRow = new TableRow(
227-
new HeimatController.Mapping(
228-
selectedTask.id(), true, true,
229-
syncMessage, "",
230-
List.of(), List.of(), "", "", 0, 0
231-
),
232-
"", 0
233-
);
226+
new HeimatController.Mapping(selectedTask.id(), true, true, syncMessage, "", List.of(), List.of(), "",
227+
"", 0, 0), "", 0);
234228
items.add(addedRow);
235229
itemsForBindings.add(addedRow);
236230
mappingTableView.scrollTo(items.size() - 1);
@@ -447,21 +441,19 @@ protected void updateItem(TableRow item, boolean empty) {
447441
}
448442

449443
TextFlow statusFlow = item.syncStatus;
450-
String status = statusFlow.getChildren().stream()
444+
String status = statusFlow.getChildren()
445+
.stream()
451446
.filter(n -> n instanceof Text)
452447
.map(n -> ((Text) n).getText())
453448
.collect(Collectors.joining());
454449

455-
456-
457450
if (!item.bookingHint.isEmpty().get()) {
458451
statusFlow = new TextFlow(statusFlow);
459452
tooltip.setText(status + "\n" + item.bookingHint.get());
460453
Text icon = new Text("ⓘ ");
461454
icon.setStyle("-fx-text-fill: #1c2070; -fx-font-size: 14px;");
462455
statusFlow.getChildren().add(0, icon);
463-
}
464-
else {
456+
} else {
465457
tooltip.setText(status);
466458
}
467459

@@ -651,7 +643,7 @@ private void setUpTimeSpinner(final Spinner<LocalTime> spinner) {
651643
}
652644
});
653645

654-
newScene.addEventFilter(KeyEvent.KEY_PRESSED, event ->{
646+
newScene.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
655647
if (event.getCode() == KeyCode.SHIFT) {
656648
shiftDown.set(true);
657649
}
@@ -737,6 +729,25 @@ public void setStage(final Stage thisStage) {
737729
this.thisStage = thisStage;
738730
}
739731

732+
/**
733+
* Converts a StyledMessage to a TextFlow for UI display.
734+
*
735+
* @param styledMessage
736+
* The styled message to convert
737+
* @return A TextFlow with properly styled text segments
738+
*/
739+
private static TextFlow convertStyledMessageToTextFlow(StyledMessage styledMessage) {
740+
TextFlow textFlow = new TextFlow();
741+
for (StyledMessage.TextSegment segment : styledMessage.getSegments()) {
742+
Text text = new Text(segment.text());
743+
if (segment.bold()) {
744+
text.setStyle("-fx-font-weight: bold;");
745+
}
746+
textFlow.getChildren().add(text);
747+
}
748+
return textFlow;
749+
}
750+
740751
public static class TableRow {
741752
private final HeimatController.Mapping mapping;
742753

@@ -755,7 +766,7 @@ public static class TableRow {
755766
public TableRow(HeimatController.Mapping mapping, String userNotes, final long userSeconds) {
756767
this.mapping = mapping;
757768
this.shouldSyncCheckBox = new SimpleBooleanProperty(mapping.shouldBeSynced());
758-
this.syncStatus = mapping.syncMessage();
769+
this.syncStatus = convertStyledMessageToTextFlow(mapping.syncMessage());
759770
this.bookingHint = new SimpleStringProperty(mapping.bookingHint());
760771

761772
this.keeptimeNotes = new SimpleStringProperty(mapping.keeptimeNotes());

0 commit comments

Comments
 (0)