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.atlas.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.atlas.data.analysis.buckets.MoneyWiseXAnalysis;
25  import io.github.tonywasher.joceanus.moneywise.atlas.data.analysis.buckets.MoneyWiseXAnalysisTransCategoryBucket;
26  import io.github.tonywasher.joceanus.moneywise.atlas.data.analysis.buckets.MoneyWiseXAnalysisTransCategoryBucket.MoneyWiseXAnalysisTransCategoryBucketList;
27  import io.github.tonywasher.joceanus.moneywise.atlas.views.MoneyWiseXAnalysisFilter;
28  import io.github.tonywasher.joceanus.moneywise.atlas.views.MoneyWiseXAnalysisFilter.MoneyWiseXAnalysisTransCategoryFilter;
29  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseBasicDataType;
30  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseTransCategory;
31  import io.github.tonywasher.joceanus.moneywise.data.statics.MoneyWiseTransCategoryClass;
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 MoneyWiseXTransCategoryAnalysisSelect
52          implements MoneyWiseXAnalysisFilterSelection, 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<MoneyWiseXAnalysisTransCategoryBucket> theButton;
72  
73      /**
74       * Category menu.
75       */
76      private final TethysUIScrollMenu<MoneyWiseXAnalysisTransCategoryBucket> theCategoryMenu;
77  
78      /**
79       * The active transaction categories bucket list.
80       */
81      private MoneyWiseXAnalysisTransCategoryBucketList 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 MoneyWiseXTransCategoryAnalysisSelect(final TethysUIFactory<?> pFactory) {
99          /* Create the button */
100         theButton = pFactory.buttonFactory().newScrollButton(MoneyWiseXAnalysisTransCategoryBucket.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 MoneyWiseXAnalysisTransCategoryFilter 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 MoneyWiseXAnalysis pAnalysis) {
184         /* Access buckets */
185         theCategories = pAnalysis.getTransCategories();
186 
187         /* Obtain the current category */
188         MoneyWiseXAnalysisTransCategoryBucket 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 MoneyWiseXAnalysisFilter<?, ?> pFilter) {
203         /* If this is the correct filter type */
204         if (pFilter instanceof MoneyWiseXAnalysisTransCategoryFilter myFilter) {
205             /* Obtain the filter bucket */
206             MoneyWiseXAnalysisTransCategoryBucket myCategory = myFilter.getBucket();
207 
208             /* Obtain equivalent bucket */
209             myCategory = theCategories.getMatchingCategory(myCategory.getTransactionCategory());
210 
211             /* Set the category */
212             theState.setTheCategory(myCategory);
213             theState.setDateRange(myFilter.getDateRange());
214             theState.applyState();
215         }
216     }
217 
218     /**
219      * Handle new Category.
220      */
221     private void handleNewCategory() {
222         /* Select the new category */
223         if (theState.setCategory(theButton.getValue())) {
224             theState.applyState();
225             theEventManager.fireEvent(PrometheusDataEvent.SELECTIONCHANGED);
226         }
227     }
228 
229     /**
230      * Build Category menu.
231      */
232     private void buildCategoryMenu() {
233         /* Reset the popUp menu */
234         theCategoryMenu.removeAllItems();
235 
236         /* Create a simple map for top-level categories */
237         final Map<String, TethysUIScrollSubMenu<MoneyWiseXAnalysisTransCategoryBucket>> myMap = new HashMap<>();
238 
239         /* Record active item */
240         final MoneyWiseXAnalysisTransCategoryBucket myCurrent = theState.getEventCategory();
241         TethysUIScrollItem<MoneyWiseXAnalysisTransCategoryBucket> myActive = null;
242 
243         /* Loop through the available category values */
244         final Iterator<MoneyWiseXAnalysisTransCategoryBucket> myIterator = theCategories.iterator();
245         while (myIterator.hasNext()) {
246             final MoneyWiseXAnalysisTransCategoryBucket myBucket = myIterator.next();
247 
248             /* Only process low-level items */
249             final MoneyWiseTransCategoryClass myClass = myBucket.getTransactionCategoryType().getCategoryClass();
250             if (myClass.canParentCategory()) {
251                 continue;
252             }
253 
254             /* Determine menu to add to */
255             final MoneyWiseTransCategory myCategory = myBucket.getTransactionCategory();
256             final MoneyWiseTransCategory myParent = myCategory.getParentCategory();
257             final String myParentName = myParent.getName();
258             final TethysUIScrollSubMenu<MoneyWiseXAnalysisTransCategoryBucket> myMenu = myMap.computeIfAbsent(myParentName, theCategoryMenu::addSubMenu);
259 
260             /* Create a new MenuItem and add it to the popUp */
261             final TethysUIScrollItem<MoneyWiseXAnalysisTransCategoryBucket> myItem = myMenu.getSubMenu().addItem(myBucket, myCategory.getSubCategory());
262 
263             /* If this is the active category */
264             if (myBucket.equals(myCurrent)) {
265                 /* Record it */
266                 myActive = myItem;
267             }
268         }
269 
270         /* Ensure active item is visible */
271         if (myActive != null) {
272             myActive.scrollToItem();
273         }
274     }
275 
276     /**
277      * SavePoint values.
278      */
279     private final class MoneyWiseEventState {
280         /**
281          * The active EventCategoryBucket.
282          */
283         private MoneyWiseXAnalysisTransCategoryBucket theCategory;
284 
285         /**
286          * The dateRange.
287          */
288         private OceanusDateRange theDateRange;
289 
290         /**
291          * The active Filter.
292          */
293         private MoneyWiseXAnalysisTransCategoryFilter theFilter;
294 
295         /**
296          * Constructor.
297          */
298         private MoneyWiseEventState() {
299         }
300 
301         /**
302          * Constructor.
303          *
304          * @param pState state to copy from
305          */
306         private MoneyWiseEventState(final MoneyWiseEventState pState) {
307             /* Initialise state */
308             theCategory = pState.getEventCategory();
309             theDateRange = pState.getDateRange();
310             theFilter = pState.getFilter();
311         }
312 
313         /**
314          * Obtain the EventCategory Bucket.
315          *
316          * @return the EventCategory
317          */
318         private MoneyWiseXAnalysisTransCategoryBucket getEventCategory() {
319             return theCategory;
320         }
321 
322         /**
323          * Obtain the dateRange.
324          *
325          * @return the dateRange
326          */
327         private OceanusDateRange getDateRange() {
328             return theDateRange;
329         }
330 
331         /**
332          * Obtain the EventCategory Filter.
333          *
334          * @return the EventCategory
335          */
336         private MoneyWiseXAnalysisTransCategoryFilter getFilter() {
337             return theFilter;
338         }
339 
340         /**
341          * Set new Category.
342          *
343          * @param pCategory the Category
344          * @return true/false did a change occur
345          */
346         private boolean setCategory(final MoneyWiseXAnalysisTransCategoryBucket pCategory) {
347             /* Adjust the selected category */
348             if (!MetisDataDifference.isEqual(pCategory, theCategory)) {
349                 setTheCategory(pCategory);
350                 return true;
351             }
352             return false;
353         }
354 
355         /**
356          * Set the Category.
357          *
358          * @param pCategory the Category
359          */
360         private void setTheCategory(final MoneyWiseXAnalysisTransCategoryBucket pCategory) {
361             /* Store the selected category */
362             theCategory = pCategory;
363             if (theCategory != null) {
364                 theFilter = new MoneyWiseXAnalysisTransCategoryFilter(theCategory);
365                 theFilter.setDateRange(theDateRange);
366             } else {
367                 theFilter = null;
368             }
369         }
370 
371 
372         /**
373          * Set the dateRange.
374          *
375          * @param pRange the dateRange
376          */
377         private void setDateRange(final OceanusDateRange pRange) {
378             /* Store the dateRange */
379             theDateRange = pRange;
380             if (theFilter != null) {
381                 theFilter.setDateRange(theDateRange);
382             }
383         }
384 
385         /**
386          * Apply the State.
387          */
388         private void applyState() {
389             /* Adjust the lock-down */
390             setEnabled(true);
391             theButton.setValue(theCategory);
392         }
393     }
394 }