New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
library: Add ListModel Entry #361
Conversation
What I'm trying to achieve is when i click on add items button it should add rows with labels to the existing listbox widget |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does that make more sense?
@andyholmes would it be good to demonstrate search function as well? P.S:
|
There's no The main principle to demonstrate here is to have a single Your add/remove buttons should call Then if you want, you implement searching and filtering for either container by wrapping the |
I'll make the required changes |
Implemented for the following changes:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Much better, I think you're getting how model-view-controller works in GTK now!
With regard to your idea about a text entry, let's clean the UI up to use the Gtk.Stack
as in the review suggestions, then how about adding a third page like Edit
or something?
A third page could be a list box, flow box, or we could demonstrate the items-changed
callback if you're feeling adventurous. Either way, for the third view we should put the Gtk.StringList
inside another model like Gtk.FilterListModel
, and have the entry change the filter.
Box { | ||
orientation: horizontal; | ||
halign: center; | ||
margin-bottom: 24; | ||
styles ["linked"] | ||
|
||
ToggleButton toggle_list_box { | ||
active: true; | ||
label: _("ListBox"); | ||
} | ||
|
||
ToggleButton toggle_flow_box { | ||
active: false; | ||
label: _("FlowBox"); | ||
group: toggle_list_box; | ||
} | ||
} | ||
|
||
Revealer reveal_list_box { | ||
transition-duration: 300; | ||
transition-type: slide_up; | ||
margin-bottom: 24; | ||
reveal-child: true; | ||
|
||
Box { | ||
halign: center; | ||
|
||
ListBox list_box { | ||
activate-on-single-click: true; | ||
selection-mode: none; | ||
width-request: 360; | ||
|
||
styles ["boxed-list"] | ||
} | ||
} | ||
} | ||
|
||
Revealer reveal_flow_box { | ||
transition-duration: 300; | ||
transition-type: slide_up; | ||
margin-bottom: 24; | ||
|
||
Box { | ||
halign: center; | ||
|
||
FlowBox flow_box { | ||
width-request: 360; | ||
activate-on-single-click: true; | ||
orientation: horizontal; | ||
selection-mode: none; | ||
styles ["card"] | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's clean this all up with some higher-level widget.
Box { | |
orientation: horizontal; | |
halign: center; | |
margin-bottom: 24; | |
styles ["linked"] | |
ToggleButton toggle_list_box { | |
active: true; | |
label: _("ListBox"); | |
} | |
ToggleButton toggle_flow_box { | |
active: false; | |
label: _("FlowBox"); | |
group: toggle_list_box; | |
} | |
} | |
Revealer reveal_list_box { | |
transition-duration: 300; | |
transition-type: slide_up; | |
margin-bottom: 24; | |
reveal-child: true; | |
Box { | |
halign: center; | |
ListBox list_box { | |
activate-on-single-click: true; | |
selection-mode: none; | |
width-request: 360; | |
styles ["boxed-list"] | |
} | |
} | |
} | |
Revealer reveal_flow_box { | |
transition-duration: 300; | |
transition-type: slide_up; | |
margin-bottom: 24; | |
Box { | |
halign: center; | |
FlowBox flow_box { | |
width-request: 360; | |
activate-on-single-click: true; | |
orientation: horizontal; | |
selection-mode: none; | |
styles ["card"] | |
} | |
} | |
} | |
Gtk.StackSwitcher { | |
stack: stack; | |
halign: center; | |
} | |
Gtk.Stack stack { | |
transition-duration: 300; | |
transition-type: crossfade; | |
vexpand: true; | |
Gtk.StackPage { | |
name: "listbox"; | |
title: _("List Box"); | |
child: | |
Box { | |
halign: center; | |
ListBox list_box { | |
activate-on-single-click: true; | |
selection-mode: none; | |
width-request: 360; | |
styles ["boxed-list"] | |
} | |
}; | |
} | |
Gtk.StackPage { | |
name: "flowbox"; | |
title: _("Flow Box"); | |
child: | |
Box { | |
halign: center; | |
FlowBox flow_box { | |
width-request: 360; | |
activate-on-single-click: true; | |
orientation: horizontal; | |
selection-mode: none; | |
styles ["card"] | |
} | |
}; | |
} | |
} |
src/Library/demos/List Model/main.js
Outdated
const add = workbench.builder.get_object("add"); | ||
const remove = workbench.builder.get_object("remove"); | ||
|
||
//ListModel initialization and binding |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
//ListModel initialization and binding | |
// Model |
src/Library/demos/List Model/main.js
Outdated
//Handling for ListBox | ||
const isListBoxActive = toggle_list_box.get_active(); | ||
reveal_list_box.reveal_child = isListBoxActive; | ||
if (isListBoxActive) { | ||
add.connect("clicked", () => { | ||
const newItem = `Item ${item}`; | ||
model.append(newItem); // Append the new item as an array to the model | ||
item++; | ||
}); | ||
remove.connect("clicked", () => { | ||
const length = model.get_n_items(); | ||
model.remove(length - 1); | ||
}); | ||
} | ||
|
||
//Handling for FlowBox | ||
const isFlowBoxActive = toggle_flow_box.get_active(); | ||
reveal_flow_box.reveal_child = isFlowBoxActive; | ||
if (isFlowBoxActive) { | ||
add.connect("clicked", () => { | ||
const newItem = `Item ${item}`; | ||
model.append(newItem); // Append the new item as an array to the model | ||
item++; | ||
}); | ||
remove.connect("clicked", () => { | ||
const length = model.get_n_items(); | ||
model.remove(length - 1); | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually we only need one set of these, since we're "controlling" the "model" in a model-view-controller pattern. Stripping this part down makes that a little bit clearer.
//Handling for ListBox | |
const isListBoxActive = toggle_list_box.get_active(); | |
reveal_list_box.reveal_child = isListBoxActive; | |
if (isListBoxActive) { | |
add.connect("clicked", () => { | |
const newItem = `Item ${item}`; | |
model.append(newItem); // Append the new item as an array to the model | |
item++; | |
}); | |
remove.connect("clicked", () => { | |
const length = model.get_n_items(); | |
model.remove(length - 1); | |
}); | |
} | |
//Handling for FlowBox | |
const isFlowBoxActive = toggle_flow_box.get_active(); | |
reveal_flow_box.reveal_child = isFlowBoxActive; | |
if (isFlowBoxActive) { | |
add.connect("clicked", () => { | |
const newItem = `Item ${item}`; | |
model.append(newItem); // Append the new item as an array to the model | |
item++; | |
}); | |
remove.connect("clicked", () => { | |
const length = model.get_n_items(); | |
model.remove(length - 1); | |
}); | |
} | |
// Controller | |
add.connect("clicked", () => { | |
const new_item = `Item ${item}`; | |
model.append(new_item); | |
item++; | |
}); | |
remove.connect("clicked", () => { | |
const n_items = model.get_n_items(); | |
model.remove(n_items - 1); | |
}); |
src/Library/demos/List Model/main.js
Outdated
// Check if ListBox is Active | ||
toggle_list_box.connect("toggled", () => { | ||
const isActive = toggle_list_box.get_active(); | ||
reveal_list_box.reveal_child = isActive; | ||
if (isActive) { | ||
console.log("ListBox toggled on"); | ||
} else { | ||
console.log("ListBox toggled off"); | ||
} | ||
}); | ||
|
||
// Check if FlowBox is Active | ||
toggle_flow_box.connect("toggled", () => { | ||
const isActive = toggle_flow_box.get_active(); | ||
reveal_flow_box.reveal_child = isActive; | ||
if (isActive) { | ||
console.log("FlowBox toggled on"); | ||
} else { | ||
console.log("FlowBox toggled off"); | ||
} | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a good idea to identify when the view has changed, but let's keep it simple so it doesn't distract from the rest of the demo.
// Check if ListBox is Active | |
toggle_list_box.connect("toggled", () => { | |
const isActive = toggle_list_box.get_active(); | |
reveal_list_box.reveal_child = isActive; | |
if (isActive) { | |
console.log("ListBox toggled on"); | |
} else { | |
console.log("ListBox toggled off"); | |
} | |
}); | |
// Check if FlowBox is Active | |
toggle_flow_box.connect("toggled", () => { | |
const isActive = toggle_flow_box.get_active(); | |
reveal_flow_box.reveal_child = isActive; | |
if (isActive) { | |
console.log("FlowBox toggled on"); | |
} else { | |
console.log("FlowBox toggled off"); | |
} | |
}); | |
// View | |
stack.connect("notify::visible-child", () => { | |
if (stack.visible_child === list_box) { | |
console.log("View: List Box"); | |
} else { | |
console.log("View: Flow Box"); | |
} | |
}); |
src/Library/demos/List Model/main.js
Outdated
const toggle_list_box = workbench.builder.get_object("toggle_list_box"); | ||
const toggle_flow_box = workbench.builder.get_object("toggle_flow_box"); | ||
const reveal_list_box = workbench.builder.get_object("reveal_list_box"); | ||
const reveal_flow_box = workbench.builder.get_object("reveal_flow_box"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Much simpler 🙂
const toggle_list_box = workbench.builder.get_object("toggle_list_box"); | |
const toggle_flow_box = workbench.builder.get_object("toggle_flow_box"); | |
const reveal_list_box = workbench.builder.get_object("reveal_list_box"); | |
const reveal_flow_box = workbench.builder.get_object("reveal_flow_box"); | |
const stack = workbench.builder.get_object("stack"); |
orientation: vertical; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can let the containing box do the child spacing.
orientation: vertical; | |
orientation: vertical; | |
spacing: 24; | |
Okay, What I'm trying to do here is when the user clicks on the search button i want to check if the searched text matches one of the strings in const filter = new Gtk.StringFilter({
expression: Gtk.PropertyExpression.new(model, null, "strings"),
ignore_case: true,
match_mode: Gtk.StringFilterMatchMode.EXACT,
}); but got an error
|
So I think you're pretty close. The filter is going to be passed an item from the const search_expression = Gtk.PropertyExpression.new(Gtk.StringObject, null, "string");
const filter = new Gtk.StringFilter({
expression: search_expression,
ignore_case: true,
match_mode: Gtk.StringFilterMatchMode.SUBSTRING,
}); When we get to the search entry, let's skip the button and just filter directly: const search_entry = new Gtk.SearchEntry();
search_entry.connect("search-changed", (editable) => {
console.log("Search terms changed");
search_filter.search = editable.get_text();
// This will not work, because you are passing a string
// where a GObject is expected. Calling this function is
// also what the filter does for you, so there's no need
search_filter.match(editable.get_text());
}); Once that's done, I think we've done enough for the demo and we'll just cleanup the UX a bit. Namely I don't think any widgets should be underneath the view stack, because everything jumps around when items are added/removed/filtered. |
@andyholmes I've applied the suggested changes; talking about UX - i was thinking to maybe change the widget that populates all the 3 models? Also we need to take care of the void space created cause of the SearchEntry widget, wdyt? P.S: I've also kept the comment about |
src/Library/demos/List Model/main.js
Outdated
|
||
/* This will not work, because you are passing a string | ||
where a GObject is expected. Calling this function is | ||
also what the filter does for you, so there's no need | ||
search_filter.match(editable.get_text()); */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can remove this.
How do you mean change the widget? Do you mean have a separate
I think the reasonable place to have the entry is above the views, because they are what will change size the most. Either a margin, or just allowing the widget to shift when the stack page changes I think is okay.
Yeah, I think remove it. We're just demonstrating how to use GListModel here and how they can be wrapped in other models. We can cover actual use of filter models and sorting models in another demo. |
I meant regarding this #361 (comment) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few more notes, and thoughts I had about UX.
ListBox list_box_editable { | ||
width-request: 240; | ||
activate-on-single-click: true; | ||
selection-mode: none; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's set this to single
and mark the remove button active
when there is a selected row, then remove the item from the model with at the row's index position. I think that's sensible UX.
Box { | ||
styles ["linked"] | ||
halign: center; | ||
margin-bottom: 18; | ||
|
||
Button add { | ||
label: _("Add Items"); | ||
} | ||
Button remove { | ||
label: _("Remove Items"); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's move this above the "Edit" page's list, and connect the whole thing so it looks something like this:
Box {
orientation: vertical;
spacing: 18;
Box {
styles ["linked"]
SearchEntry search_entry {
hexpand: true;
placeholder-text: _("Start searching");
}
Button add {
icon-name: "list-add-symbolic";
tooltip-text: _("Add Item");
}
Button remove {
icon-name: "list-remove-symbolic";
tooltip-text: _("Remove Item");
}
}
ListBox list_box_editable {
activate-on-single-click: true;
selection-mode: single;
styles ["boxed-list"]
}
}
} | ||
|
||
Gtk.Stack stack { | ||
transition-duration: 300; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
transition-duration: 300; |
Not needed.
|
||
Gtk.StackSwitcher { | ||
stack: stack; | ||
halign: center; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one can stay, so the switch is nicely centered about the lists.
src/Library/demos/List Model/main.js
Outdated
|
||
model.connect("items-changed", (list, position, removed, added) => { | ||
console.log( | ||
`position: ${position}, Item removed? ${Boolean( | ||
removed, | ||
)}, Item added? ${Boolean(added)}`, | ||
); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
model.connect("items-changed", (list, position, removed, added) => { | |
console.log( | |
`position: ${position}, Item removed? ${Boolean( | |
removed, | |
)}, Item added? ${Boolean(added)}`, | |
); | |
}); |
Implemented the suggested changes:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking really good.
There are some final UI fixes, then this one is good to go. I think this will be a good template for the GtkListView
, GtkGridView
and other GListModel demos.
width-request: 360; | ||
orientation: horizontal; | ||
selection-mode: none; | ||
styles ["card"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
styles ["card"] |
title: "List Model"; | ||
description: _("List models are a simple interface for ordered lists of GObject instances"); | ||
|
||
Box { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, so let's do this:
Except for the StackSwitcher
and FlowBox items, go through and remove all the halign: center
and width-request
properties. Then put this Box
inside an Adw.Clamp
with maximum-size: 640;
so you have something like:
Adw.Clamp {
maximum-size: 640;
child:
Box current_box {
// ...
};
}
Then set hexpand: true
and valign: start
on list_box
, flow_box
and list_box_editable
. That should even up the whole demo.
styles ["linked"] | ||
|
||
SearchEntry search_entry { | ||
hexpand: true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hexpand: true; |
src/Library/demos/List Model/main.js
Outdated
}); | ||
|
||
list_box_editable.connect("row-selected", () => { | ||
remove.sensitive = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is good, but it's possible to "unselect" all rows, so better to do something like:
remove.sensitive = true; | |
remove.sensitive = list_box_editable.get_selected_row() !== null; |
src/Library/demos/List Model/main.js
Outdated
const search_entry = workbench.builder.get_object("search_entry"); | ||
|
||
//Model | ||
const model = new Gtk.StringList(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const model = new Gtk.StringList(); | |
const model = new Gtk.StringList({ | |
strings: [ | |
"Default Item 1", | |
"Default Item 2", | |
"Default Item 3", | |
], | |
}); |
I think this is the easiest way to avoid dealing with empty states in the demo. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it's obviously better than a void space
Box { | ||
halign: center; | ||
hexpand: true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can remove this Box
, the Frame
is taking its place.
src/Library/demos/List Model/main.js
Outdated
const items = model.get_n_items(); | ||
if (items === 0) { | ||
remove.sensitive = false; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should be handled by the row-selected
signal, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a really nice demo, well done 👍
I think we should definitely use this as a template for the list/grid/column view demos.
I think that was a good choice. We might want to update and expand the links later, but I think we should get into the other list model widgets first, and see how we want to organize them. |
* Adds ListModel Demo * Updated Code * Used List Model * Made Changes as Discussed * Updated Code as per review * Added new page and items-changed signal * Implemented Filter-Model * Minor Changes * Minor Change * Applied suggested Changes * Refined Code * Updated UX * More Changes * Updated Logic * Finishing Up * Updated UI * Minor Tweak * Enhanced UI and functionality * Perfect Squares * Minor tweaks and indentation fix
* Adds ListModel Demo * Updated Code * Used List Model * Made Changes as Discussed * Updated Code as per review * Added new page and items-changed signal * Implemented Filter-Model * Minor Changes * Minor Change * Applied suggested Changes * Refined Code * Updated UX * More Changes * Updated Logic * Finishing Up * Updated UI * Minor Tweak * Enhanced UI and functionality * Perfect Squares * Minor tweaks and indentation fix
Closes #294