1 package fr.ifremer.dali.ui.swing.content.home.survey;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 import fr.ifremer.dali.decorator.DecoratorService;
27 import fr.ifremer.dali.dto.DaliBeans;
28 import fr.ifremer.dali.dto.configuration.programStrategy.ProgramDTO;
29 import fr.ifremer.dali.dto.data.survey.CampaignDTO;
30 import fr.ifremer.dali.dto.enums.FilterTypeValues;
31 import fr.ifremer.dali.dto.referential.LocationDTO;
32 import fr.ifremer.dali.ui.swing.DaliUIContext;
33 import fr.ifremer.dali.ui.swing.content.home.HomeUI;
34 import fr.ifremer.dali.ui.swing.content.home.HomeUIHandler;
35 import fr.ifremer.dali.ui.swing.util.DaliUI;
36 import fr.ifremer.dali.ui.swing.util.table.AbstractDaliTableModel;
37 import fr.ifremer.dali.ui.swing.util.table.AbstractDaliTableUIHandler;
38 import fr.ifremer.dali.ui.swing.util.table.renderer.StateIconCellRenderer;
39 import fr.ifremer.dali.ui.swing.util.table.renderer.SynchronizationStatusIconCellRenderer;
40 import fr.ifremer.quadrige3.ui.swing.component.coordinate.CoordinateEditor;
41 import fr.ifremer.quadrige3.ui.swing.table.SwingTable;
42 import fr.ifremer.quadrige3.ui.swing.table.editor.ExtendedComboBoxCellEditor;
43 import fr.ifremer.quadrige3.ui.swing.table.editor.LocalTimeCellEditor;
44 import org.jdesktop.swingx.table.TableColumnExt;
45
46 import javax.swing.BorderFactory;
47 import javax.swing.SortOrder;
48 import javax.swing.SwingUtilities;
49 import java.beans.PropertyChangeListener;
50 import java.util.List;
51
52 import static org.nuiton.i18n.I18n.t;
53
54
55
56
57 public class SurveysTableUIHandler extends
58 AbstractDaliTableUIHandler<SurveysTableRowModel, SurveysTableUIModel, SurveysTableUI> {
59
60 private ExtendedComboBoxCellEditor<ProgramDTO> programCellEditor;
61 private ExtendedComboBoxCellEditor<CampaignDTO> campaignCellEditor;
62 private ExtendedComboBoxCellEditor<LocationDTO> locationCellEditor;
63 private PropertyChangeListener authenticationListener;
64
65
66
67
68 @Override
69 protected String[] getRowPropertiesToIgnore() {
70 return new String[]{
71 SurveysTableRowModel.PROPERTY_SAMPLING_OPERATIONS,
72 SurveysTableRowModel.PROPERTY_SAMPLING_OPERATIONS_LOADED,
73 SurveysTableRowModel.PROPERTY_DIRTY,
74 SurveysTableRowModel.PROPERTY_ERRORS,
75 SurveysTableRowModel.PROPERTY_CONTROL_DATE,
76 SurveysTableRowModel.PROPERTY_VALIDATION_DATE,
77 SurveysTableRowModel.PROPERTY_QUALIFICATION_COMMENT,
78 SurveysTableRowModel.PROPERTY_SYNCHRONIZATION_STATUS,
79 SurveysTableRowModel.PROPERTY_COORDINATE
80 };
81 }
82
83
84
85
86 @Override
87 public void beforeInit(final SurveysTableUI ui) {
88 super.beforeInit(ui);
89
90
91 final SurveysTableUIModel model = new SurveysTableUIModel();
92 ui.setContextValue(model);
93 }
94
95
96
97
98 @Override
99 public void afterInit(final SurveysTableUI ui) {
100
101
102 initUI(ui);
103
104
105 getUI().getDuplicateButton().setEnabled(false);
106 getUI().getEditCombo().setEnabled(false);
107 getUI().getStateCombo().setEnabled(false);
108 getUI().getDeleteButton().setEnabled(false);
109
110 enablePrivilegedControls();
111
112 createProgramCellEditor();
113 createCampaignCellEditor();
114 createLocationCellEditor();
115
116
117 initTable();
118
119
120 initActionComboBox(getUI().getEditCombo());
121
122
123 initActionComboBox(getUI().getStateCombo());
124
125
126 initListeners();
127
128
129 getUI().getNextButton().setBorder(
130 BorderFactory.createCompoundBorder(BorderFactory.createMatteBorder(2, 2, 2, 2, getConfig().getColorHighlightButtonBorder()), ui.getNextButton().getBorder())
131 );
132
133 }
134
135
136
137
138 @Override
139 protected boolean isRowValid(final SurveysTableRowModel row) {
140
141 return (!row.isEditable() || super.isRowValid(row)) && isSurveyRowValid(row);
142 }
143
144 private boolean isSurveyRowValid(SurveysTableRowModel row) {
145
146
147 if (row.getLatitudeMin() == null ^ row.getLongitudeMin() == null) {
148 DaliBeans.addError(row, t("dali.validator.error.coordinate.invalid"), SurveysTableRowModel.PROPERTY_LATITUDE_MIN, SurveysTableRowModel.PROPERTY_LONGITUDE_MIN);
149 }
150 if (row.getLatitudeMax() == null ^ row.getLongitudeMax() == null) {
151 DaliBeans.addError(row, t("dali.validator.error.coordinate.invalid"), SurveysTableRowModel.PROPERTY_LATITUDE_MAX, SurveysTableRowModel.PROPERTY_LONGITUDE_MAX);
152 }
153 if (row.getLatitudeMin() == null && row.getLatitudeMax() != null) {
154 DaliBeans.addError(row, t("dali.validator.error.coordinate.invalid"), SurveysTableRowModel.PROPERTY_LATITUDE_MIN, SurveysTableRowModel.PROPERTY_LATITUDE_MAX);
155 }
156 if (row.getLongitudeMin() == null && row.getLongitudeMax() != null) {
157 DaliBeans.addError(row, t("dali.validator.error.coordinate.invalid"), SurveysTableRowModel.PROPERTY_LONGITUDE_MIN, SurveysTableRowModel.PROPERTY_LONGITUDE_MAX);
158 }
159
160
161 if (row.getLatitudeMin() != null && row.getLongitudeMin() != null && row.getPositioning() == null) {
162 DaliBeans.addError(row, t("dali.validator.error.positioning.required"), SurveysTableRowModel.PROPERTY_POSITIONING);
163 }
164
165
166 if (!row.isOperationsValid()) {
167 DaliBeans.addError(row, t("dali.home.survey.table.operationsInvalid"));
168 }
169
170 boolean hasNoError = DaliBeans.hasNoBlockingError(row);
171
172 if (!hasNoError) {
173 ensureColumnsWithErrorAreVisible(row);
174 }
175
176 return hasNoError;
177 }
178
179
180
181
182 private void initTable() {
183
184
185 TableColumnExt programCol = addColumn(
186 programCellEditor,
187 newTableCellRender(SurveysTableModel.PROGRAM),
188 SurveysTableModel.PROGRAM);
189 programCol.setSortable(true);
190 programCol.setPreferredWidth(200);
191
192
193 TableColumnExt campaignCol = addColumn(
194 campaignCellEditor,
195 newTableCellRender(SurveysTableModel.CAMPAIGN),
196 SurveysTableModel.CAMPAIGN);
197 campaignCol.setSortable(true);
198 campaignCol.setPreferredWidth(200);
199
200
201 TableColumnExt locationCol = addColumn(
202 locationCellEditor,
203 newTableCellRender(SurveysTableModel.LOCATION),
204 SurveysTableModel.LOCATION);
205 locationCol.setSortable(true);
206 locationCol.setPreferredWidth(200);
207
208
209 TableColumnExt dateCol = addLocalDatePickerColumnToModel(SurveysTableModel.DATE, getConfig().getDateFormat());
210 dateCol.setSortable(true);
211 dateCol.setPreferredWidth(50);
212
213
214 TableColumnExt nameCol = addColumn(SurveysTableModel.NAME);
215 nameCol.setSortable(true);
216 nameCol.setPreferredWidth(100);
217
218
219 addCommentColumn(SurveysTableModel.COMMENT);
220
221
222 TableColumnExt synchroStatusCol = addColumn(
223 null,
224 new SynchronizationStatusIconCellRenderer(getContext()),
225 SurveysTableModel.SYNCHRONIZATION_STATUS);
226 synchroStatusCol.setSortable(true);
227 synchroStatusCol.setPreferredWidth(20);
228
229
230 TableColumnExt stateCol = addColumn(
231 null,
232 new StateIconCellRenderer(getContext()),
233 SurveysTableModel.STATE);
234 stateCol.setSortable(true);
235 stateCol.setPreferredWidth(100);
236
237
238 TableColumnExt bottomDepthCol = addColumn(SurveysTableModel.BOTTOM_DEPTH);
239 bottomDepthCol.setCellEditor(newNumberCellEditor(Double.class, false, DaliUI.DECIMAL2_PATTERN));
240 bottomDepthCol.setCellRenderer(newNumberCellRenderer(2));
241 bottomDepthCol.setSortable(true);
242 bottomDepthCol.setPreferredWidth(100);
243
244
245 TableColumnExt latitudeMinCol = addCoordinateColumnToModel(
246 CoordinateEditor.CoordinateType.LATITUDE_MIN,
247 SurveysTableModel.LATITUDE_MIN);
248 latitudeMinCol.setSortable(true);
249 latitudeMinCol.setPreferredWidth(100);
250
251
252 TableColumnExt longitudeMinCol = addCoordinateColumnToModel(
253 CoordinateEditor.CoordinateType.LONGITUDE_MIN,
254 SurveysTableModel.LONGITUDE_MIN);
255 longitudeMinCol.setSortable(true);
256 longitudeMinCol.setPreferredWidth(100);
257
258
259 TableColumnExt latitudeMaxCol = addCoordinateColumnToModel(
260 CoordinateEditor.CoordinateType.LATITUDE_MAX,
261 SurveysTableModel.LATITUDE_MAX);
262 latitudeMaxCol.setSortable(true);
263 latitudeMaxCol.setPreferredWidth(100);
264
265
266 TableColumnExt longitudeMaxCol = addCoordinateColumnToModel(
267 CoordinateEditor.CoordinateType.LONGITUDE_MAX,
268 SurveysTableModel.LONGITUDE_MAX);
269 longitudeMaxCol.setSortable(true);
270 longitudeMaxCol.setPreferredWidth(100);
271
272
273 TableColumnExt positioningCol = addExtendedComboDataColumnToModel(
274 SurveysTableModel.POSITIONING,
275 getContext().getReferentialService().getPositioningSystems(),
276 false);
277 positioningCol.setSortable(true);
278 positioningCol.setPreferredWidth(100);
279
280
281 TableColumnExt positioningPrecisionCol = addColumn(
282 SurveysTableModel.POSITIONING_PRECISION);
283 positioningPrecisionCol.setSortable(true);
284 positioningPrecisionCol.setPreferredWidth(100);
285 positioningPrecisionCol.setEditable(false);
286
287
288 TableColumnExt recorderDepartmentCol = addColumn(
289 null,
290 newTableCellRender(SurveysTableModel.RECORDER_DEPARTMENT),
291 SurveysTableModel.RECORDER_DEPARTMENT
292 );
293 recorderDepartmentCol.setSortable(true);
294 recorderDepartmentCol.setPreferredWidth(200);
295 recorderDepartmentCol.setEditable(false);
296
297
298 final TableColumnExt timeCol = addColumn(
299 new LocalTimeCellEditor(),
300 newTableCellRender(Integer.class, DecoratorService.TIME_IN_HOURS_MINUTES),
301 SurveysTableModel.TIME);
302 timeCol.setMaxWidth(100);
303 timeCol.setWidth(100);
304 timeCol.setSortable(true);
305
306
307 TableColumnExt updateDateCol = addColumn(null, newDateCellRenderer(getConfig().getDateTimeFormat()), SurveysTableModel.UPDATE_DATE);
308 updateDateCol.setSortable(true);
309 updateDateCol.setPreferredWidth(50);
310
311
312 TableColumnExt controlDateCol = addColumn(null, newDateCellRenderer(getConfig().getDateTimeFormat()), SurveysTableModel.CONTROL_DATE);
313 controlDateCol.setSortable(true);
314 controlDateCol.setPreferredWidth(50);
315
316
317 TableColumnExt validationDateCol = addColumn(null, newDateCellRenderer(getConfig().getDateTimeFormat()), SurveysTableModel.VALIDATION_DATE);
318 validationDateCol.setSortable(true);
319 validationDateCol.setPreferredWidth(50);
320
321
322 TableColumnExt validationCommentCol = addCommentColumn(SurveysTableModel.VALIDATION_COMMENT, SurveysTableRowModel.PROPERTY_VALIDATION_COMMENT, false);
323
324
325 TableColumnExt qualificationDateCol = addColumn(null, newDateCellRenderer(getConfig().getDateTimeFormat()), SurveysTableModel.QUALIFICATION_DATE);
326 qualificationDateCol.setSortable(true);
327 qualificationDateCol.setPreferredWidth(50);
328
329
330 TableColumnExt qualificationCommentCol = addCommentColumn(SurveysTableModel.QUALIFICATION_COMMENT, SurveysTableRowModel.PROPERTY_QUALIFICATION_COMMENT, false);
331
332
333 TableColumnExt qualityFlagCol = addColumn(SurveysTableModel.QUALITY_FLAG);
334 qualityFlagCol.setPreferredWidth(50);
335
336 SurveysTableModel tableModel = new SurveysTableModel(getTable().getColumnModel());
337 getTable().setModel(tableModel);
338
339
340 tableModel.setNoneEditableCols(
341 SurveysTableModel.SYNCHRONIZATION_STATUS,
342 SurveysTableModel.STATE,
343 SurveysTableModel.UPDATE_DATE,
344 SurveysTableModel.CONTROL_DATE,
345 SurveysTableModel.VALIDATION_DATE,
346 SurveysTableModel.QUALIFICATION_DATE,
347 SurveysTableModel.QUALITY_FLAG);
348
349
350 dateCol.setHideable(false);
351 programCol.setHideable(false);
352
353
354 initTable(getTable());
355
356
357 recorderDepartmentCol.setVisible(false);
358 bottomDepthCol.setVisible(false);
359 latitudeMinCol.setVisible(false);
360 longitudeMinCol.setVisible(false);
361 latitudeMaxCol.setVisible(false);
362 longitudeMaxCol.setVisible(false);
363 positioningCol.setVisible(false);
364 positioningPrecisionCol.setVisible(false);
365 timeCol.setVisible(false);
366 updateDateCol.setVisible(false);
367 controlDateCol.setVisible(false);
368 validationDateCol.setVisible(false);
369 validationCommentCol.setVisible(false);
370 qualificationDateCol.setVisible(false);
371 qualificationCommentCol.setVisible(false);
372 qualityFlagCol.setVisible(false);
373
374
375 getTable().setSortOrder(SurveysTableModel.LOCATION, SortOrder.ASCENDING);
376
377
378 addEditionPanelBorder();
379
380 }
381
382 private void createProgramCellEditor() {
383
384 programCellEditor = newExtendedComboBoxCellEditor(null, ProgramDTO.class, false);
385
386 programCellEditor.setAction("unfilter", "dali.common.unfilter", e -> {
387 if (!askBefore(t("dali.common.unfilter"), t("dali.common.unfilter.confirmation"))) {
388 return;
389 }
390
391 updateProgramCellEditor(getModel().getSingleSelectedRow(), true);
392 getTable().requestFocus();
393 });
394
395 }
396
397 private void updateProgramCellEditor(SurveysTableRowModel row, boolean forceNoFilter) {
398
399 updateProgramCellEditor(row, forceNoFilter, true);
400 }
401
402 private void updateProgramCellEditor(SurveysTableRowModel row, boolean forceNoFilter, boolean canResetValue) {
403
404
405 if (forceNoFilter) row.setForceNoProgramFilter(true);
406 if (row.isForceNoProgramFilter()) forceNoFilter = true;
407
408 programCellEditor.getCombo().setActionEnabled(!forceNoFilter && getContext().getDataContext().isContextFiltered(FilterTypeValues.PROGRAM));
409
410 List<ProgramDTO> availablePrograms =
411 getContext().getObservationService().getAvailablePrograms(
412 row.getLocation() == null ? null : row.getLocation().getId(),
413 row.getDate(), forceNoFilter);
414
415
416 if (canResetValue && row.isEditable() && row.getProgram() != null && !availablePrograms.contains(row.getProgram())) {
417 row.setProgram(null);
418 }
419 programCellEditor.getCombo().setData(availablePrograms);
420
421 }
422
423 private void createCampaignCellEditor() {
424
425 campaignCellEditor = newExtendedComboBoxCellEditor(null, CampaignDTO.class, false);
426
427 campaignCellEditor.setAction("unfilter", "dali.common.unfilter", e -> {
428 if (!askBefore(t("dali.common.unfilter"), t("dali.common.unfilter.confirmation"))) {
429 return;
430 }
431
432 updateCampaignCellEditor(getModel().getSingleSelectedRow(), true);
433 getTable().requestFocus();
434 });
435
436 }
437
438 private void updateCampaignCellEditor(SurveysTableRowModel row, boolean forceNoFilter) {
439
440 updateCampaignCellEditor(row, forceNoFilter, true);
441 }
442
443 private void updateCampaignCellEditor(SurveysTableRowModel row, boolean forceNoFilter, boolean canResetValue) {
444
445
446 if (forceNoFilter) row.setForceNoCampaignFilter(true);
447 if (row.isForceNoCampaignFilter()) forceNoFilter = true;
448
449 campaignCellEditor.getCombo().setActionEnabled(!forceNoFilter && getContext().getDataContext().isContextFiltered(FilterTypeValues.CAMPAIGN));
450
451 List<CampaignDTO> availableCampaigns =
452 getContext().getObservationService().getAvailableCampaigns(row.getDate(), forceNoFilter);
453
454
455 if (canResetValue && row.isEditable() && row.getCampaign() != null && !availableCampaigns.contains(row.getCampaign())) {
456 row.setCampaign(null);
457 }
458
459 campaignCellEditor.getCombo().setData(availableCampaigns);
460
461 }
462
463 private void createLocationCellEditor() {
464
465 locationCellEditor = newExtendedComboBoxCellEditor(null, LocationDTO.class, false);
466
467 locationCellEditor.setAction("unfilter", "dali.common.unfilter", e -> {
468 if (!askBefore(t("dali.common.unfilter"), t("dali.common.unfilter.confirmation"))) {
469 return;
470 }
471
472 updateLocationCellEditor(getModel().getSingleSelectedRow(), true);
473 getTable().requestFocus();
474 });
475
476 }
477
478 private void updateLocationCellEditor(SurveysTableRowModel row, boolean forceNoFilter) {
479
480 updateLocationCellEditor(row, forceNoFilter, true);
481 }
482
483 private void updateLocationCellEditor(SurveysTableRowModel row, boolean forceNoFilter, boolean canResetValue) {
484
485
486 if (forceNoFilter) row.setForceNoLocationFilter(true);
487 if (row.isForceNoLocationFilter()) forceNoFilter = true;
488
489 locationCellEditor.getCombo().setActionEnabled(!forceNoFilter && getContext().getDataContext().isContextFiltered(FilterTypeValues.LOCATION));
490
491 List<LocationDTO> availableLocations =
492 getContext().getObservationService().getAvailableLocations(
493 null,
494 row.getProgram() == null ? null : row.getProgram().getCode(),
495 forceNoFilter);
496
497
498 if (canResetValue && row.isEditable() && row.getLocation() != null && !availableLocations.contains(row.getLocation())) {
499 row.setLocation(null);
500 }
501 locationCellEditor.getCombo().setData(availableLocations);
502 }
503
504 private void initListeners() {
505
506
507 getTable().addPropertyChangeListener("tableCellEditor", evt -> {
508 if (evt.getNewValue() != null) {
509
510
511 final SurveysTableRowModel survey = getModel().getSingleSelectedRow();
512
513
514 if (survey != null) {
515 updateProgramCellEditor(survey, false, false);
516 updateCampaignCellEditor(survey, false, false);
517 updateLocationCellEditor(survey, false, false);
518 }
519
520 }
521 });
522
523
524 authenticationListener = evt -> enablePrivilegedControls();
525
526 getContext().addPropertyChangeListener(DaliUIContext.PROPERTY_AUTHENTICATION_TOOLTIPTEXT, authenticationListener);
527
528
529 getTable().addPropertyChangeListener(SwingTable.PROPERTY_SORTING, evt ->
530 getModel().getMainUIModel().getMapUIModel().setBuildMapOnSelectionChanged(!getTable().isSorting()));
531 }
532
533 private void enablePrivilegedControls() {
534
535 getUI().getValidateButton().setEnabled(getContext().getDataContext().isRecorderValidator());
536 getUI().getUnvalidateButton().setEnabled(getContext().getDataContext().isRecorderValidator());
537 getUI().getQualifyButton().setEnabled(getContext().getDataContext().isRecorderQualifier());
538 }
539
540 @Override
541 public void onCloseUI() {
542
543
544 getContext().removePropertyChangeListener(DaliUIContext.PROPERTY_AUTHENTICATION_TOOLTIPTEXT, authenticationListener);
545
546 super.onCloseUI();
547 }
548
549
550
551
552 @Override
553 protected void onRowModified(int rowIndex, SurveysTableRowModel row, String propertyName, Integer propertyIndex, Object oldValue, Object newValue) {
554 super.onRowModified(rowIndex, row, propertyName, propertyIndex, oldValue, newValue);
555
556 row.setDirty(true);
557
558
559
560
561
562
563
564
565
566
567 if (SurveysTableRowModel.PROPERTY_PROGRAM.equals(propertyName)) {
568
569 updateLocationCellEditor(row, false);
570
571
572 selectSurvey(row);
573 }
574
575
576 if (SurveysTableRowModel.PROPERTY_LOCATION.equals(propertyName) || SurveysTableRowModel.PROPERTY_DATE.equals(propertyName)) {
577
578 updateProgramCellEditor(row, false);
579
580 if (SurveysTableRowModel.PROPERTY_DATE.equals(propertyName)) {
581 updateCampaignCellEditor(row, false);
582 }
583
584
585 selectSurvey(row);
586 }
587
588
589 if (SurveysTableRowModel.PROPERTY_LATITUDE_MIN.equals(propertyName)
590 || SurveysTableRowModel.PROPERTY_LATITUDE_MAX.equals(propertyName)
591 || SurveysTableRowModel.PROPERTY_LONGITUDE_MIN.equals(propertyName)
592 || SurveysTableRowModel.PROPERTY_LONGITUDE_MAX.equals(propertyName)
593 ) {
594
595 if (row.getLatitudeMin() == null && row.getLongitudeMin() == null
596 && row.getLatitudeMax() == null && row.getLongitudeMax() == null) {
597 row.setPositioning(null);
598 getTable().repaint();
599 }
600 }
601
602 forceRevalidateModel();
603 }
604
605 private void selectSurvey(SurveysTableRowModel survey) {
606
607 getModel().getMainUIModel().setSelectedSurvey(survey);
608 }
609
610
611
612
613 @Override
614 public AbstractDaliTableModel<SurveysTableRowModel> getTableModel() {
615 return (SurveysTableModel) getTable().getModel();
616 }
617
618
619
620
621 @Override
622 public SwingTable getTable() {
623 return ui.getSurveysTable();
624 }
625
626
627
628
629 @Override
630 protected void onRowsAdded(List<SurveysTableRowModel> addedRows) {
631 super.onRowsAdded(addedRows);
632
633
634 if (addedRows.size() == 1) {
635 SurveysTableRowModel row = addedRows.get(0);
636
637
638 row.setSynchronizationStatus(getContext().getSystemService().getLocalShare());
639
640
641 Integer recorderDepId = getContext().getDataContext().getRecorderDepartmentId();
642 if (recorderDepId != null) {
643 row.setDepartment(getContext().getReferentialService().getDepartmentById(recorderDepId));
644 }
645
646
647 setFocusOnCell(row);
648 }
649 }
650
651
652
653
654 public void onNext() {
655
656 if (getTable().isEditing()) {
657 getTable().getCellEditor().stopCellEditing();
658 }
659
660 getModel().getMainUIModel().getOperationsTableUIModel().addNewRow();
661
662 }
663
664 public HomeUIHandler getHomeUIHandler() {
665 return getParentContainer(HomeUI.class).getHandler();
666 }
667
668 public void showMap() {
669 getHomeUIHandler().showSurveysMap();
670 }
671
672 public void hideMap() {
673 getHomeUIHandler().showSamplingOperations();
674 }
675
676 public void beforeLoad() {
677 uninstallSaveTableStateListener();
678 }
679
680 public void afterLoad() {
681 restoreTableState();
682 SwingUtilities.invokeLater(this::installSaveTableStateListener);
683 }
684 }