View Javadoc
1   /*
2    * MoneyWise: Finance Application
3    * Copyright 2012-2026. Tony Washer
4    *
5    * Licensed under the Apache License, Version 2.0 (the "License"); you may not
6    * use this file except in compliance with the License.  You may obtain a copy
7    * of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
14   * License for the specific language governing permissions and limitations under
15   * the License.
16   */
17  package io.github.tonywasher.joceanus.moneywise.lethe.ui.controls;
18  
19  import io.github.tonywasher.joceanus.oceanus.date.OceanusDateRange;
20  import io.github.tonywasher.joceanus.oceanus.event.OceanusEventManager;
21  import io.github.tonywasher.joceanus.oceanus.event.OceanusEventRegistrar;
22  import io.github.tonywasher.joceanus.oceanus.event.OceanusEventRegistrar.OceanusEventProvider;
23  import io.github.tonywasher.joceanus.metis.data.MetisDataDifference;
24  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseBasicDataType;
25  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseTransCategory;
26  import io.github.tonywasher.joceanus.moneywise.data.statics.MoneyWiseTransCategoryClass;
27  import io.github.tonywasher.joceanus.moneywise.lethe.data.analysis.data.MoneyWiseAnalysis;
28  import io.github.tonywasher.joceanus.moneywise.lethe.data.analysis.data.MoneyWiseAnalysisTransCategoryBucket;
29  import io.github.tonywasher.joceanus.moneywise.lethe.data.analysis.data.MoneyWiseAnalysisTransCategoryBucket.MoneyWiseAnalysisTransCategoryBucketList;
30  import io.github.tonywasher.joceanus.moneywise.lethe.views.MoneyWiseAnalysisFilter;
31  import io.github.tonywasher.joceanus.moneywise.lethe.views.MoneyWiseAnalysisFilter.MoneyWiseAnalysisTransCategoryFilter;
32  import io.github.tonywasher.joceanus.prometheus.views.PrometheusDataEvent;
33  import io.github.tonywasher.joceanus.tethys.api.base.TethysUIComponent;
34  import io.github.tonywasher.joceanus.tethys.api.base.TethysUIConstant;
35  import io.github.tonywasher.joceanus.tethys.api.base.TethysUIEvent;
36  import io.github.tonywasher.joceanus.tethys.api.button.TethysUIScrollButtonManager;
37  import io.github.tonywasher.joceanus.tethys.api.control.TethysUILabel;
38  import io.github.tonywasher.joceanus.tethys.api.factory.TethysUIFactory;
39  import io.github.tonywasher.joceanus.tethys.api.menu.TethysUIScrollItem;
40  import io.github.tonywasher.joceanus.tethys.api.menu.TethysUIScrollMenu;
41  import io.github.tonywasher.joceanus.tethys.api.menu.TethysUIScrollSubMenu;
42  import io.github.tonywasher.joceanus.tethys.api.pane.TethysUIBoxPaneManager;
43  
44  import java.util.HashMap;
45  import java.util.Iterator;
46  import java.util.Map;
47  
48  /**
49   * Transaction Category Analysis Selection.
50   */
51  public class MoneyWiseTransCategoryAnalysisSelect
52          implements MoneyWiseAnalysisFilterSelection, OceanusEventProvider<PrometheusDataEvent> {
53      /**
54       * Text for TransCategory Label.
55       */
56      private static final String NLS_CATEGORY = MoneyWiseBasicDataType.TRANSCATEGORY.getItemName();
57  
58      /**
59       * The Event Manager.
60       */
61      private final OceanusEventManager<PrometheusDataEvent> theEventManager;
62  
63      /**
64       * The panel.
65       */
66      private final TethysUIBoxPaneManager thePanel;
67  
68      /**
69       * The select button.
70       */
71      private final TethysUIScrollButtonManager<MoneyWiseAnalysisTransCategoryBucket> theButton;
72  
73      /**
74       * Category menu.
75       */
76      private final TethysUIScrollMenu<MoneyWiseAnalysisTransCategoryBucket> theCategoryMenu;
77  
78      /**
79       * The active transaction categories bucket list.
80       */
81      private MoneyWiseAnalysisTransCategoryBucketList theCategories;
82  
83      /**
84       * The state.
85       */
86      private MoneyWiseEventState theState;
87  
88      /**
89       * The savePoint.
90       */
91      private MoneyWiseEventState theSavePoint;
92  
93      /**
94       * Constructor.
95       *
96       * @param pFactory the GUI factory
97       */
98      protected MoneyWiseTransCategoryAnalysisSelect(final TethysUIFactory<?> pFactory) {
99          /* Create the button */
100         theButton = pFactory.buttonFactory().newScrollButton(MoneyWiseAnalysisTransCategoryBucket.class);
101 
102         /* Create the label */
103         final TethysUILabel myLabel = pFactory.controlFactory().newLabel(NLS_CATEGORY + TethysUIConstant.STR_COLON);
104 
105         /* Create Event Manager */
106         theEventManager = new OceanusEventManager<>();
107 
108         /* Define the layout */
109         thePanel = pFactory.paneFactory().newHBoxPane();
110         thePanel.addSpacer();
111         thePanel.addNode(myLabel);
112         thePanel.addNode(theButton);
113 
114         /* Create initial state */
115         theState = new MoneyWiseEventState();
116         theState.applyState();
117 
118         /* Access the menus */
119         theCategoryMenu = theButton.getMenu();
120 
121         /* Create the listeners */
122         final OceanusEventRegistrar<TethysUIEvent> myRegistrar = theButton.getEventRegistrar();
123         myRegistrar.addEventListener(TethysUIEvent.NEWVALUE, e -> handleNewCategory());
124         theButton.setMenuConfigurator(e -> buildCategoryMenu());
125     }
126 
127     @Override
128     public TethysUIComponent getUnderlying() {
129         return thePanel;
130     }
131 
132     @Override
133     public OceanusEventRegistrar<PrometheusDataEvent> getEventRegistrar() {
134         return theEventManager.getEventRegistrar();
135     }
136 
137     @Override
138     public MoneyWiseAnalysisTransCategoryFilter getFilter() {
139         return theState.getFilter();
140     }
141 
142     @Override
143     public boolean isAvailable() {
144         return theCategories != null
145                 && !theCategories.isEmpty();
146     }
147 
148     /**
149      * Create SavePoint.
150      */
151     public void createSavePoint() {
152         /* Create the savePoint */
153         theSavePoint = new MoneyWiseEventState(theState);
154     }
155 
156     /**
157      * Restore SavePoint.
158      */
159     public void restoreSavePoint() {
160         /* Restore the savePoint */
161         theState = new MoneyWiseEventState(theSavePoint);
162 
163         /* Apply the state */
164         theState.applyState();
165     }
166 
167     @Override
168     public void setEnabled(final boolean bEnabled) {
169         /* Pass call on to button */
170         theButton.setEnabled(bEnabled && isAvailable());
171     }
172 
173     @Override
174     public void setVisible(final boolean pVisible) {
175         thePanel.setVisible(pVisible);
176     }
177 
178     /**
179      * Set analysis.
180      *
181      * @param pAnalysis the analysis.
182      */
183     public void setAnalysis(final MoneyWiseAnalysis pAnalysis) {
184         /* Access buckets */
185         theCategories = pAnalysis.getTransCategories();
186 
187         /* Obtain the current category */
188         MoneyWiseAnalysisTransCategoryBucket myCategory = theState.getEventCategory();
189 
190         /* Switch to versions from the analysis */
191         myCategory = myCategory != null
192                 ? theCategories.getMatchingCategory(myCategory.getTransactionCategory())
193                 : theCategories.getDefaultCategory();
194 
195         /* Set the category */
196         theState.setTheCategory(myCategory);
197         theState.setDateRange(pAnalysis.getDateRange());
198         theState.applyState();
199     }
200 
201     @Override
202     public void setFilter(final MoneyWiseAnalysisFilter<?, ?> pFilter) {
203         /* If this is the correct filter type */
204         if (pFilter instanceof MoneyWiseAnalysisTransCategoryFilter) {
205             /* Access filter */
206             final MoneyWiseAnalysisTransCategoryFilter myFilter = (MoneyWiseAnalysisTransCategoryFilter) pFilter;
207 
208             /* Obtain the filter bucket */
209             MoneyWiseAnalysisTransCategoryBucket myCategory = myFilter.getBucket();
210 
211             /* Obtain equivalent bucket */
212             myCategory = theCategories.getMatchingCategory(myCategory.getTransactionCategory());
213 
214             /* Set the category */
215             theState.setTheCategory(myCategory);
216             theState.setDateRange(myFilter.getDateRange());
217             theState.applyState();
218         }
219     }
220 
221     /**
222      * Handle new Category.
223      */
224     private void handleNewCategory() {
225         /* Select the new category */
226         if (theState.setCategory(theButton.getValue())) {
227             theState.applyState();
228             theEventManager.fireEvent(PrometheusDataEvent.SELECTIONCHANGED);
229         }
230     }
231 
232     /**
233      * Build Category menu.
234      */
235     private void buildCategoryMenu() {
236         /* Reset the popUp menu */
237         theCategoryMenu.removeAllItems();
238 
239         /* Create a simple map for top-level categories */
240         final Map<String, TethysUIScrollSubMenu<MoneyWiseAnalysisTransCategoryBucket>> myMap = new HashMap<>();
241 
242         /* Record active item */
243         final MoneyWiseAnalysisTransCategoryBucket myCurrent = theState.getEventCategory();
244         TethysUIScrollItem<MoneyWiseAnalysisTransCategoryBucket> myActive = null;
245 
246         /* Loop through the available category values */
247         final Iterator<MoneyWiseAnalysisTransCategoryBucket> myIterator = theCategories.iterator();
248         while (myIterator.hasNext()) {
249             final MoneyWiseAnalysisTransCategoryBucket myBucket = myIterator.next();
250 
251             /* Only process low-level items */
252             final MoneyWiseTransCategoryClass myClass = myBucket.getTransactionCategoryType().getCategoryClass();
253             if (myClass.canParentCategory()) {
254                 continue;
255             }
256 
257             /* Determine menu to add to */
258             final MoneyWiseTransCategory myCategory = myBucket.getTransactionCategory();
259             final MoneyWiseTransCategory myParent = myCategory.getParentCategory();
260             final String myParentName = myParent.getName();
261             final TethysUIScrollSubMenu<MoneyWiseAnalysisTransCategoryBucket> myMenu = myMap.computeIfAbsent(myParentName, theCategoryMenu::addSubMenu);
262 
263             /* Create a new MenuItem and add it to the popUp */
264             final TethysUIScrollItem<MoneyWiseAnalysisTransCategoryBucket> myItem = myMenu.getSubMenu().addItem(myBucket, myCategory.getSubCategory());
265 
266             /* If this is the active category */
267             if (myBucket.equals(myCurrent)) {
268                 /* Record it */
269                 myActive = myItem;
270             }
271         }
272 
273         /* Ensure active item is visible */
274         if (myActive != null) {
275             myActive.scrollToItem();
276         }
277     }
278 
279     /**
280      * SavePoint values.
281      */
282     private final class MoneyWiseEventState {
283         /**
284          * The active EventCategoryBucket.
285          */
286         private MoneyWiseAnalysisTransCategoryBucket theCategory;
287 
288         /**
289          * The dateRange.
290          */
291         private OceanusDateRange theDateRange;
292 
293         /**
294          * The active Filter.
295          */
296         private MoneyWiseAnalysisTransCategoryFilter theFilter;
297 
298         /**
299          * Constructor.
300          */
301         private MoneyWiseEventState() {
302         }
303 
304         /**
305          * Constructor.
306          *
307          * @param pState state to copy from
308          */
309         private MoneyWiseEventState(final MoneyWiseEventState pState) {
310             /* Initialise state */
311             theCategory = pState.getEventCategory();
312             theDateRange = pState.getDateRange();
313             theFilter = pState.getFilter();
314         }
315 
316         /**
317          * Obtain the EventCategory Bucket.
318          *
319          * @return the EventCategory
320          */
321         private MoneyWiseAnalysisTransCategoryBucket getEventCategory() {
322             return theCategory;
323         }
324 
325         /**
326          * Obtain the dateRange.
327          *
328          * @return the dateRange
329          */
330         private OceanusDateRange getDateRange() {
331             return theDateRange;
332         }
333 
334         /**
335          * Obtain the EventCategory Filter.
336          *
337          * @return the EventCategory
338          */
339         private MoneyWiseAnalysisTransCategoryFilter getFilter() {
340             return theFilter;
341         }
342 
343         /**
344          * Set new Category.
345          *
346          * @param pCategory the Category
347          * @return true/false did a change occur
348          */
349         private boolean setCategory(final MoneyWiseAnalysisTransCategoryBucket pCategory) {
350             /* Adjust the selected category */
351             if (!MetisDataDifference.isEqual(pCategory, theCategory)) {
352                 setTheCategory(pCategory);
353                 return true;
354             }
355             return false;
356         }
357 
358         /**
359          * Set the Category.
360          *
361          * @param pCategory the Category
362          */
363         private void setTheCategory(final MoneyWiseAnalysisTransCategoryBucket pCategory) {
364             /* Store the selected category */
365             theCategory = pCategory;
366             if (theCategory != null) {
367                 theFilter = new MoneyWiseAnalysisTransCategoryFilter(theCategory);
368                 theFilter.setDateRange(theDateRange);
369             } else {
370                 theFilter = null;
371             }
372         }
373 
374 
375         /**
376          * Set the dateRange.
377          *
378          * @param pRange the dateRange
379          */
380         private void setDateRange(final OceanusDateRange pRange) {
381             /* Store the dateRange */
382             theDateRange = pRange;
383             if (theFilter != null) {
384                 theFilter.setDateRange(theDateRange);
385             }
386         }
387 
388         /**
389          * Apply the State.
390          */
391         private void applyState() {
392             /* Adjust the lock-down */
393             setEnabled(true);
394             theButton.setValue(theCategory);
395         }
396     }
397 }