Kitlist
A list manager for maintaining kit lists
Loading...
Searching...
No Matches
kitlist_ftxui.cpp
Go to the documentation of this file.
1/*
2
3 This file is part of Kitlist, a program to maintain a simple list
4 of items and assign items to one or more categories.
5
6 Copyright (C) 2008-2025 Frank Dean
7
8 Kitlist is free software: you can redistribute it and/or modify
9 it under the terms of the GNU General Public License as published by
10 the Free Software Foundation, either version 3 of the License, or
11 (at your option) any later version.
12
13 Kitlist is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 GNU General Public License for more details.
17
18 You should have received a copy of the GNU General Public License
19 along with Kitlist. If not, see <http://www.gnu.org/licenses/>.
20
21*/
22#include "kitlist_ftxui.hpp"
23#include "kitparser.hpp"
24#include <ftxui/component/captured_mouse.hpp>
25#include <ftxui/component/component.hpp>
26#include <ftxui/component/component_base.hpp>
27#include <ftxui/component/component_options.hpp>
28#include <ftxui/component/screen_interactive.hpp>
29#include <ftxui/dom/elements.hpp>
30#include <ftxui/screen/screen.hpp>
31#include <filesystem>
32#include <iostream>
33#include <string>
34#include <vector>
35
36using namespace std;
37using namespace ftxui;
38using namespace fdsd::ftxui;
39
41{
42 if (refresh_select_category_list == true) {
43 auto categories = get_categories();
44 selected_categories.clear();
45 for (auto& c : categories) {
46 SelectCategory sc(false, c);
47 selected_categories.push_back(sc);
48 }
50 }
51}
52
54{
57 copy_category_container->DetachAllChildren();
58 for (auto& sc : selected_categories) {
59 copy_category_container->Add(Checkbox(sc.category->name, &sc.selected));
60 }
62 }
63 Component component = Container::Vertical({copy_category_container});
64
65 component |= Renderer([&](Element inner) {
67 copy_category_container->DetachAllChildren();
68 for (auto& sc : selected_categories) {
69 copy_category_container->Add(Checkbox(sc.category->name, &sc.selected));
70 }
72 }
73 Element document = vbox({
74 inner,
75 });
76 return document
77 | vscroll_indicator | frame;
78 });
79 return component;
80}
81
83{
84 auto ok_button = Button("Ok", [&] {
85 layer = static_cast<int>(Layer::default_layer);
87 vector<shared_ptr<Item>> items;
88 for (const auto& i : all_items) {
89 if (i->checked)
90 items.push_back(i);
91 }
92 vector<shared_ptr<Category>> categories;
93 for (const auto& sc : selected_categories) {
94 if (sc.selected)
95 categories.push_back(sc.category);
96 }
97 if (!items.empty() && !categories.empty()) {
98 copy_items_to_categories(items, categories);
99 show_save = true;
100 }
101 });
102 auto cancel_button = Button("Cancel", [&] {
103 layer = static_cast<int>(Layer::default_layer);
104 });
105 Component category_list = select_category_list();
106 auto buttons = Container::Horizontal({ok_button, cancel_button});
107
108 buttons |= Renderer([&](Element inner) {
109 Element document = vbox({
110 separator(),
111 inner,
112 });
113 return document;
114 });
115
116 Component component = Container::Vertical({
117 category_list | flex,
118 buttons,
119 });
120
121 component |= Renderer([&](Element inner) {
122 Element document = vbox({
123 text("Select categories "),
124 separator(),
125 inner,
126 });
127 return document
128 | size(WIDTH, GREATER_THAN, 40)
129 | size(HEIGHT, GREATER_THAN, 8)
130 | border;
131 });
132 return component;
133}
134
136{
137 Component component = Renderer(item_action_container, [&] {
138 item_action_container->DetachAllChildren();
139 auto action_button = Button(get_selected_category() == Model::no_category ? "Delete" : "Remove",
140 [&] {
143 } else {
145 }
146 show_save = true;
147 layer = static_cast<int>(Layer::default_layer);
148 });
149 auto cancel_button = Button("Cancel", [&] { layer = static_cast<int>(Layer::default_layer); });
150 item_action_container->Add(action_button);
151 item_action_container->Add(cancel_button);
152 Element document = vbox({
153 text(get_selected_category() ? "Delete checked items" : "Remove checked items"),
154 separator(),
155 hbox({
156 action_button->Render(),
157 cancel_button->Render(),
158 }),
159 });
160 return document
161 | size(WIDTH, GREATER_THAN, 40)
162 | border;
163 });
164 return component;
165}
166
168 std::function<void()> cancel,
169 std::function<void()> create_category)
170{
171 InputOption input_option = InputOption::Default();
172 input_option.multiline = false;
173 input_option.transform = [](InputState state) {
174
175 state.element |= color(Color::Black);
176
177 if (state.is_placeholder) {
178 state.element |= dim;
179 }
180
181 if (state.focused || state.hovered) {
182 state.element |= inverted;
183 }
184 return state.element;
185 };
186
187 auto component = Container::Vertical({
188 Container::Horizontal({
189 Input(&category_name, "Enter name", input_option) | borderEmpty,
190 }),
191 Container::Horizontal({
192 Button("OK", create_category),
193 Button("Cancel", cancel),
194 }) | flex,
195 });
196 // Polish how the components are rendered
197 component |= Renderer([&](Element inner) {
198 return vbox({
199 text("Category editor"),
200 separator(),
201 hbox({
202 inner,
203 }),
204 })
205 | size(WIDTH, GREATER_THAN, 40)
206 | border;
207 });
208 return component;
209}
210
212 std::function<void()> cancel,
213 std::function<void()> create_item)
214{
215 InputOption input_option = InputOption::Default();
216 input_option.multiline = false;
217 input_option.transform = [](InputState state) {
218
219 state.element |= color(Color::Black);
220
221 if (state.is_placeholder) {
222 state.element |= dim;
223 }
224
225 if (state.focused || state.hovered) {
226 state.element |= inverted;
227 }
228 return state.element;
229 };
230
231 auto component = Container::Vertical({
232 Container::Horizontal({
233 Checkbox("Checked", &item_checked) | borderEmpty,
234 Input(&item_name, "Enter name", input_option) | borderEmpty,
235 }),
236 Container::Horizontal({
237 Button("OK", create_item),
238 Button("Cancel", cancel),
239 }) | flex,
240 });
241 // Polish how the components are rendered
242 component |= Renderer([&](Element inner) {
243 return vbox({
244 text("Item editor"),
245 separator(),
246 hbox({
247 inner,
248 }),
249 })
250 | size(WIDTH, GREATER_THAN, 40)
251 | border;
252 });
253 return component;
254}
255
257{
258 if (!filename.empty()) {
259 try {
261 } catch (const KitListBaseApp::file_not_found& e) {
262 cerr << "File not found: \"" << filename << "\" - " << e.what() << '\n';
263 exit(1);
264 } catch (const KitParser::parse_exception& e) {
265 cerr << "Error parsing file: \"" << filename << "\" - " << e.what() << '\n';
266 exit(1);
267 }
268 }
269
270 auto categories = get_categories();
271 for (auto& c : categories) {
272 SelectCategory sc(false, c);
273 selected_categories.push_back(sc);
274 }
275
276 auto screen = ScreenInteractive::Fullscreen();
277
278 // ---------- Button Container ----------
279
280 vector<string> filter_entries = {
281 "Show all",
282 "Checked",
283 "Unchecked",
284 };
285
286 auto button_container = Container::Vertical({
287 Container::Horizontal({
288 Button("Quit", screen.ExitLoopClosure()),
289 Maybe(Button("Save", [&] {
290 save();
291 show_save = false;
292 }), [&] { return show_save; }),
293
294 Button("Check all", [&] {
296 show_save = true;
297 }),
298
299 Button("Uncheck all", [&] {
301 show_save = true;
302 }),
303
304 Button("Toggle all", [&] {
306 show_save = true;
307 }),
308 Dropdown(&filter_entries, &selected_filter),
309 }),
310 Container::Horizontal({
311 Button("New category", [&] { layer = static_cast<int>(Layer::new_category_layer); }),
312 Button("New item", [&] { layer = static_cast<int>(Layer::new_item_layer); }),
313 Button(get_selected_category() == Model::no_category ? "Item action" : "Remove checked items",
314 [&] { layer = static_cast<int>(Layer::item_action_layer); }),
315 Maybe(Button("Delete category", [&] {
317 show_save = true;
321 }), [&] { return get_selected_category() != Model::no_category; }),
322 Maybe(Button("Copy to categories", [&] {
324 layer = static_cast<int>(Layer::copy_category_layer);
325 }), [&] { return count_filter_items_for_current_selected_category() > 0; }),
326 }),
327 });
328
329 // button_container |= Renderer([&](Element inner) {
330 // return vbox({
331 // text("Main component"),
332 // separator(),
333 // inner,
334 // })
335 // | size(WIDTH, GREATER_THAN, 20)
336 // | size(HEIGHT, GREATER_THAN, 20)
337 // | border
338 // | center;
339 // });
340
341 Component button_container_wrap = Renderer(button_container, [&] {
342 return button_container->Render();
343 });
344
345 // ---------- Category Menu ----------
346
347 Component category_container = Container::Vertical({});
348
349 // When the category is re-rendered, update the model with the currently
350 // selected category.
351 Component category_container_wrap = Renderer(category_container, [&] {
352
354 category_container->DetachAllChildren();
355 category_menu_entries.clear();
356
357 // First entry is to show all items
359
361 for (const auto& c : current_categories)
362 category_menu_entries.push_back(c->name);
363
364 // Assigning a Vertical menu option and setting the transform as follows,
365 // achieves the same end result as simply defining MenuOption as:
366 // MenuOption option;
367 //
368 // Note that not passing a menu option results in it defaulting to
369 // MenuOption::Vertical(), which shows inactive and non-focuses menu items
370 // dimmed out. So, specifically creating what we want in case someone in
371 // future changes the defaults (as they are a bit inconsistent).
372 MenuOption category_menu_option = MenuOption::Vertical();
373
374 category_menu_option.entries_option.transform = [] (const EntryState& state) {
375 Element e = text((state.active ? "> " : " ") + state.label);
376 if (state.focused) {
377 e |= inverted;
378 }
379 if (state.active) {
380 e |= bold;
381 }
382 // if (!state.focused && !state.active) {
383 // e |= dim;
384 // }
385 return e;
386 };
387
388 auto menu = Menu(&category_menu_entries, &menu_selected_category, category_menu_option);
389
390 category_container->Add(menu);
392 }
393
394 auto current_category = get_category(get_selected_category());
397 } else {
398 auto category = current_categories[menu_selected_category -1];
399 select_category(category->get_id());
400 }
401 return category_container->Render();
402 });
403
404
405 // ---------- Item Container ----------
406
407 Component item_container = Container::Vertical({});
408 Component item_container_wrap = Renderer(item_container, [&] {
409 item_container->DetachAllChildren();
410 switch (selected_filter) {
411 case 1:
413 break;
414 case 2:
416 break;
417 case 0:
418 [[fallthrough]];
419 default:
421 }
423 for (const auto& i : items) {
424 CheckboxOption cbx_option;
425 cbx_option.on_change = [&] {
426 set_dirty();
427 show_save = true;
428 };
429 item_container->Add(Checkbox(i->get_name(), &i->checked, cbx_option));
430 }
431 return item_container->Render();
432 });
433
434 // ---------- Combined Container ----------
435
436 auto inner_container = Container::Horizontal({ category_container_wrap, item_container_wrap });
437 auto layer_0_container = Container::Vertical({ button_container_wrap, inner_container });
438 auto new_item_dialog = new_item_component(
439 [&] { // Cancel
440 layer = static_cast<int>(Layer::default_layer);
441 },
442 [&] { // Ok
443 layer = static_cast<int>(Layer::default_layer);
444 if (item_name.empty())
445 return;
447 item_name = "";
448 item_checked = false;
449 show_save = true;
450 });
451
452 auto layer_1_container = Container::Horizontal({new_item_dialog});
453
454 auto layer_1_renderer = Renderer(layer_1_container, [&] {
455 return
456 layer_1_container->Render();
457 });
458
459 auto new_category_dialog = new_category_component(
460 [&] { // Cancel
461 layer = static_cast<int>(Layer::default_layer);
462 },
463 [&] { // Ok
464 layer = static_cast<int>(Layer::default_layer);
465 if (category_name.empty())
466 return;
468 category_name = "";
469 show_save = true;
473 });
474
475 auto layer_2_container = Container::Horizontal({new_category_dialog});
476 auto layer_2_renderer = Renderer(layer_2_container, [&] {
477 return
478 layer_2_container->Render();
479 });
480
481 auto layer_3_container = Container::Horizontal({item_action_dialog()});
482 auto layer_3_renderer = Renderer(layer_3_container, [&] {
483 return
484 layer_3_container->Render();
485 });
486
488 auto layer_4_container = Container::Horizontal({copy_items_dialog()});
489
490 auto layer_4_renderer = Renderer(layer_4_container, [&] {
491 return layer_4_container->Render();
492 });
493
494// ---------- Renderer ----------
495
496 auto layer_0_renderer = Renderer(layer_0_container, [&] {
497
498 return
499 vbox({
500 hbox({
501 button_container_wrap->Render(),
502 }),
503 hbox({
504 category_container_wrap->Render() | vscroll_indicator | frame | border,
505 item_container_wrap->Render() | vscroll_indicator | frame | border | xflex,
506 }) | yflex,
507 });
508 });
509
510 auto main_container = Container::Tab(
511 {
512 layer_0_renderer,
513 layer_1_renderer,
514 layer_2_renderer,
515 layer_3_renderer,
516 layer_4_renderer,
517 },
518 &layer);
519
520 auto main_renderer = Renderer(main_container, [&] {
521 Element document = layer_0_renderer->Render();
522
523 switch (layer) {
524 case 1:
525 document = dbox({
526 document,
527 layer_1_renderer->Render() | clear_under | center,
528 });
529 break;
530 case 2:
531 document = dbox({
532 document,
533 layer_2_renderer->Render() | clear_under | center,
534 });
535 break;
536 case 3:
537 document = dbox({
538 document,
539 layer_3_renderer->Render() | clear_under | center,
540 });
541 break;
542 case 4:
543 document = dbox({
544 document,
545 layer_4_renderer->Render() | clear_under | center,
546 });
547 break;
548 }
549 return document;
550 });
551
552 screen.Loop(main_renderer);
553 return 0;
554
555}
Exception throw when a file is not found.
virtual const char * what() const override
Describes the exeption.
void set_dirty(bool dirty=true) const
auto count_filter_items_for_current_selected_category() const
Returns a count of the filtered items in the currently selected category.
const std::vector< std::shared_ptr< Item > > filter_items_for_current_selected_category() const
Filters the list of Item instances based on the currently selected category (Model::selected_category...
void delete_all_current_checked_items()
Deletes all checked items.
void set_filter(Model::state_filter state) const
Sets filtering of checked items.
int32_t get_selected_category() const
Gets the ID of the currently selected Category.
std::string filename
The current filename the Model was loaded from.
void copy_items_to_categories(std::vector< std::shared_ptr< Item > > &items, std::vector< std::shared_ptr< Category > > &categories) const
Copies all of the passed items to each of the passed categories, updating the model relationships.
void delete_category(int32_t id) const
Deletes the Category having the passed ID.
int32_t new_item(const std::string &name, bool checked) const
creates a new Item in the model using the passed parameters, incrementing Model::max_item_id and assi...
virtual void load_file(const std::string &filename)
Loads the specified file.
void select_category(int32_t id)
Sets the selected Category to that of the passed ID.
void remove_all_current_checked_items()
Removes all checked items from the current category.
const std::vector< std::shared_ptr< Item > > get_all_items_for_current_selected_category() const
Returns all items for the currently selected Category.
const std::shared_ptr< Category > get_category(int32_t id) const
Gets a Category by ID.
const std::string all_items_text
Constant string indicating the option to select no category and therefor display all items.
const std::vector< std::shared_ptr< Category > > get_categories() const
Gets the list of categories.
int32_t new_category(const std::string &name) const
Creates a new Category in the Model using the passed parameters, incrementing Model::max_category_id ...
virtual bool save()
Saves the current Model using the current filename.
void change_all_current_items_checked_states(Model::check_action action) const
Switches the checked state of all items in the currently selected Category.
XML parsing error.
Definition kitparser.hpp:86
@ check
Definition model.hpp:119
@ uncheck
Definition model.hpp:120
@ toggle
Definition model.hpp:118
static const int32_t no_category
Indicates no filtering by category.
Definition model.hpp:153
@ unchecked
Show only unchecked items.
Definition model.hpp:110
@ all
Show all items, i.e. no filtering.
Definition model.hpp:106
@ checked
Show only checked items.
Definition model.hpp:108
std::vector< std::string > category_menu_entries
The menu entries for the catalog list.
::ftxui::Component copy_category_container
Component for modal dialog for copying items to categories.
::ftxui::Component item_action_dialog()
Component to display options for removing or deleting checked items.
std::vector< std::shared_ptr< Category > > current_categories
The current list of Category instances.
bool refresh_category_container
When true, the category container needs to be refreshed.
void do_refresh_select_category_list()
refreshes the select category list.
std::string item_name
Updated by new_item_component.
bool refresh_select_category_list
When true, the select category list needs to be refreshed.
int selected_filter
Maintained by dropdown giving choices of Model::state_filter.
::ftxui::Component new_item_component(std::function< void()> cancel, std::function< void()> create_item)
Component for modal dialog for the user to create a new Item.
::ftxui::Component item_action_container
Component for modal dialog for delete or removing items.
std::string category_name
Updated by new_category_component.
::ftxui::Component copy_items_dialog()
Modal dialog for selecting target categories.
bool refresh_copy_item_category_container
When true, the copy items to category container needs to be refreshed.
bool show_save
Whether the Save button is shown.
bool item_checked
Updated by new_item_component.
::ftxui::Component select_category_list()
Component displaying a list of all categories with a checkbox.
::ftxui::Component new_category_component(std::function< void()> cancel, std::function< void()> create_item)
Component for modal dialog for the user to create a new Category.
int run(const std::string filename)
The main run loop.
std::vector< SelectCategory > selected_categories
int menu_selected_category
The index position in the list of category menu items.
A namespace for the FTXUI user interface.
An element used in lists for selecting categories.
Copyright 2008-2025 Frank Dean