Text doesn't show up on GtkLabel - gtk

I have this code:
using Gtk;
class ConquerLauncher : Gtk.Application {
protected override void activate() {
var window = new ApplicationWindow(this);
window.add(new InputList());
window.show_all();
}
}
public int main(string[] args) {
return new ConquerLauncher().run(args);
}
class InputList : Box {
public InputList() {
this.set_orientation(Orientation.VERTICAL);
var listStore = new Gtk.ListStore(1,typeof(Label));
var treeView = new TreeView.with_model(listStore);
this.pack_start(treeView);
this.pack_start(new InputBox(this,listStore));
var column = new TreeViewColumn();
column.set_title("Foo bar's");
treeView.append_column(column);
treeView.set_model(listStore);
}
}
class InputBox : Box {
public InputBox(InputList list,Gtk.ListStore store) {
this.set_orientation(Orientation.HORIZONTAL);
var entry = new Entry();
this.pack_start(entry);
var button = new Button.with_label("Add foo bar");
this.pack_start(button);
button.clicked.connect(() => {
var text = entry.text;
entry.text = "";
TreeIter tp;
store.append(out tp);
store.set(tp, 0, new Label(text), -1);
this.show_all();
list.show_all();
});
}
}
What I want to do:
I want to create some sort of input form consisting of three elements:
In an Entry the user should write some text. If the presses the button, it should be added to the TreeView with a ListStore as a model.
Expectation:
The user enters a text, presses the button, the contents of the Entry are added to the TreeView and the text input field is cleared.
Reality:
Everything works, except the label that is added to the TreeView is blank.
Before:
Here an example input:
After pressing the button:

Related

Create WinUI3/MVVM Most Recently Used (MRU) List in Menu Bar

I would like to create a classic "Recent Files" list in my Windows app menu bar (similar to Visual Studio's menu bar -> File -> Recent Files -> see recent files list)
The MRU list (List < string > myMRUList...) is known and is not in focus of this question. The problem is how to display and bind/interact with the list according to the MVVM rules.
Microsoft.Toolkit.Uwp.UI.Controls's Menu class will be removed in a future release and they recommend to use MenuBar control from the WinUI. I haven't found any examples, that use WinUI's MenuBar to create a "Recent Files" list.
I'm using Template Studio to create a WinUI 3 app. In the ShellPage.xaml I added
<MenuFlyoutSubItem x:Name="mruFlyout" Text="Recent Files"></MenuFlyoutSubItem>
and in ShellPage.xaml.c
private void Button_Click(object sender, RoutedEventArgs e)
{
mruFlyout.Items.Insert(mruFlyout.Items.Count, new MenuFlyoutItem(){ Text = "C:\\Test1_" + DateTime.Now.ToString("MMMM dd") } );
mruFlyout.Items.Insert(mruFlyout.Items.Count, new MenuFlyoutItem(){ Text = "C:\\Test2_" + DateTime.Now.ToString("MMMM dd") } );
mruFlyout.Items.Insert(mruFlyout.Items.Count, new MenuFlyoutItem(){ Text = "C:\\Test3_" + DateTime.Now.ToString("MMMM dd") } );
}
knowing this is not MVVM, but even this approach does not work properly, because the dynamically generated MenuFlyoutItem can be updated only once by Button_Click() event.
Could anybody give me an example, how to create the "Recent Files" functionality, but any help would be great! Thanks
Unfortunately, it seems that there is no better solution than handling this in code behind since the Items collection is readonly and also doesn't response to changes in the UI Layout.
In addition to that, note that because of https://github.com/microsoft/microsoft-ui-xaml/issues/7797, updating the Items collection does not get reflected until the Flyout has been closed and reopened.
So assuming your ViewModel has an ObservableCollection, I would probably do this:
// 1. Register collection changed
MyViewModel.RecentFiles.CollectionChanged += RecentFilesChanged;
// 2. Handle collection change
private void RecentFilesChanged(object sender, NotifyCollectionChangedEventArgs args)
{
// 3. Create new UI collection
var flyoutItems = list.Select(entry =>
new MenuFlyoutItem()
{
Text = entry.Name
}
);
// 4. Updating your MenuFlyoutItem
mruFlyout.Items.Clear();
flyoutItems.ForEach(entry => mruFlyout.Items.Add(entry));
}
Based on chingucoding's answer I got to the "recent files list" binding working.
For completeness I post the detailed code snippets here (keep in mind, that I'm not an expert):
Again using Template Studio to create a WinUI 3 app.
ShellViewModel.cs
// constructor
public ShellViewModel(INavigationService navigationService, ILocalSettingsService localSettingsService)
{
...
MRUUpdateItems();
}
ShellViewModel_RecentFiles.cs ( <-- partial class )
using System.Collections.ObjectModel;
using System.ComponentModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Windows.Storage;
using Windows.Storage.AccessCache;
using Windows.Storage.Pickers;
namespace App_MostRecentUsedTest.ViewModels;
public partial class ShellViewModel : ObservableRecipient
{
public ObservableCollection<MRUItem> MRUItems{ get; set;} = new();
// update ObservableCollection<MRUItem>MRUItems from MostRecentlyUsedList
public void MRUUpdateItems()
{
var mruTokenList = StorageApplicationPermissions.MostRecentlyUsedList.Entries.Select(entry => entry.Token).ToList();
var mruMetadataList = StorageApplicationPermissions.MostRecentlyUsedList.Entries.Select(entry => entry.Metadata).ToList(); // contains path as string
MRUItems.Clear(); var i = 0;
foreach (var path in mruMetadataList)
{
MRUItems.Add(new MRUItem() { Path = path, Token = mruTokenList[i++] });
}
}
// called if user selects a recent used file from menu bar list
[RelayCommand]
protected async Task MRULoadFileClicked(int? fileId)
{
if (fileId is not null)
{
var mruItem = MRUItems[(int)fileId];
FileInfo fInfo = new FileInfo(mruItem.Path ?? "");
if (fInfo.Exists)
{
StorageFile? file = await Windows.Storage.AccessCache.StorageApplicationPermissions.MostRecentlyUsedList.GetFileAsync(mruItem.Token);
if (file is not null)
{
Windows.Storage.AccessCache.StorageApplicationPermissions.MostRecentlyUsedList.Add(file, file.Path); // store file.Path into Metadata
MRUUpdateItems();
// LOAD_FILE(file);
}
}
else
{
}
}
await Task.CompletedTask;
}
[RelayCommand]
protected async Task MenuLoadFileClicked()
{
StorageFile? file = await GetFilePathAsync();
if (file is not null)
{
Windows.Storage.AccessCache.StorageApplicationPermissions.MostRecentlyUsedList.Add(file, file.Path); // store file.Path into Metadata
MRUUpdateItems();
// LOAD_FILE(file);
}
await Task.CompletedTask;
}
// get file path with filePicker
private async Task<StorageFile?> GetFilePathAsync()
{
FileOpenPicker filePicker = new();
filePicker.FileTypeFilter.Add(".txt");
IntPtr hwnd = WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindow);
WinRT.Interop.InitializeWithWindow.Initialize(filePicker, hwnd);
return await filePicker.PickSingleFileAsync();
}
public class MRUItem : INotifyPropertyChanged
{
private string? path;
private string? token;
public string? Path
{
get => path;
set
{
path = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(path));
}
}
public string? Token
{
get => token;
set => token = value;
}
public event PropertyChangedEventHandler? PropertyChanged;
}
}
ShellPage.xaml
<MenuBar>
<MenuBarItem x:Name="ShellMenuBarItem_File">
<MenuFlyoutItem x:Uid="ShellMenuItem_File_Load" Command="{x:Bind ViewModel.MenuLoadFileClickedCommand}" />
<MenuFlyoutSubItem x:Name="MRUFlyout" Text="Recent Files..." />
</MenuBarItem>
</MenuBar>
ShellPage.xaml.cs
// constructor
public ShellPage(ShellViewModel viewModel)
{
...
// MRU initialziation
// assign RecentFilesChanged() to CollectionChanged-event
ViewModel.MRUItems.CollectionChanged += RecentFilesChanged;
// Add (and RemoveAt) trigger RecentFilesChanged-event to update MenuFlyoutItems
ViewModel.MRUItems.Add(new MRUItem() { Path = "", Token = ""});
ViewModel.MRUItems.RemoveAt(ViewModel.MRUItems.Count - 1);
}
// MRU Handle collection change
private void RecentFilesChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
// project each MRUItems list element into a new UI MenuFlyoutItem flyoutItems list
var i = 0;
var flyoutItems = ViewModel.MRUItems.Select(entry =>
new MenuFlyoutItem()
{
Text = " " + i.ToString() + " " + FilenameHelper.EllipsisString(entry.Path, 65),
Command = ViewModel.MRULoadFileClickedCommand,
CommandParameter = i++
}
);
//// If you want to update the list while it is shown,
//// you will need to create a new FlyoutItem because of
//// https://github.com/microsoft/microsoft-ui-xaml/issues/7797
// Create a new flyout and populate it
var newFlyout = new MenuFlyoutSubItem();
newFlyout.Text = MRUFlyout.Text; // Text="Recent Files...";
// Updating your MenuFlyoutItem
flyoutItems.ToList().ForEach(item => newFlyout.Items.Add(item));
// Get index of old sub item and remove it
var oldIndex = ShellMenuBarItem_File.Items.IndexOf(MRUFlyout);
ShellMenuBarItem_File.Items.Remove(MRUFlyout);
// Insert the new flyout at the correct position
ShellMenuBarItem_File.Items.Insert(oldIndex, newFlyout);
// Assign newFlyout to "old"-MRUFlyout
MRUFlyout = newFlyout;
}

Gtk.Stack won't change visible child inside an event callback function

So I am currently working on an app for elementary os and encountered a problem.
I have a window which has a Granite.Sourcelistview on the left and a stack holding the different views on the right. My problem is that when pressing a button on one of the screens (the project settings screen i created), the stack should change the current view to a different one but it doesn't. The current view stays and is not changed.
This is the window:
public class MainWindow : Gtk.Window {
private SourceListStackView srcl_view {get; set;}
construct {
var header = new Gtk.HeaderBar ();
header.show_close_button = true;
//this is the source list view
srcl_view = new SourceListStackView ();
var paned = new Gtk.Paned (Gtk.Orientation.HORIZONTAL);
paned.position = 130;
paned.pack1 (srcl_view, false, false);
paned.add2 (srcl_view.stack);
add(paned);
set_titlebar (header);
}
public static int main(string[] args) {
Gtk.init (ref args);
MainWindow app = new MainWindow ();
app.show_all ();
Gtk.main ();
return 0;
}
}
This is the sourcelistview class that i created:
public class SourceListStackView : Granite.Widgets.SourceList {
public Gtk.Stack stack {get; set;}
public SourceListStackView () {
var project_page = new ProjectSettings ();
stack = new Gtk.Stack ();
var project = new Granite.Widgets.SourceList.ExpandableItem("Root");
this.root.add(project);
stack.add_named(project_page, "hello");
//here depending on what item of the sourcelist is created,
//the view with the same name as the item
//should be displayed (not the best mechanism but works)
this.item_selected.connect ((item) => {
if(item != null){
stack.visible_child_name = item.name;
}
});
//problematic part is here: This won't change the current view..why?
//The button should add another item to the
// source list view and change the current view
// to the newly created Welcome Screen but it doesn't do that..
project_page.button.clicked.connect(() => {
project.add(new Granite.Widgets.SourceList.Item ("Welcome"));
stack.add_named(new Granite.Widgets.Welcome("bla bli blu", "bla"), "Welcome");
stack.set_visible_child_name("Welcome");
});
}
}
This is the view with the button that should trigger the change of the view:
public class ProjectSettings : Granite.SimpleSettingsPage {
public Gtk.Button button {get; set;}
public ProjectSettings () {
Object (
activatable: false,
description: "This is a screen",
header: "",
icon_name: "preferences-system",
title: "Screen"
);
}
construct {
var project_name_label = new Gtk.Label ("Name");
project_name_label.xalign = 1;
var project_name_entry = new Gtk.Entry ();
project_name_entry.hexpand = true;
project_name_entry.placeholder_text = "Peter";
content_area.attach (project_name_label, 0, 0, 1, 1);
content_area.attach (project_name_entry, 1, 0, 1, 1);
button = new Gtk.Button.with_label ("Save Settings");
action_area.add (button);
}
}
The part that does not work is this one:
//problematic part is here: This won't change the current view.. why?
project_page.button.clicked.connect(() => {
project.add(new Granite.Widgets.SourceList.Item ("Welcome"));
stack.add_named(new Granite.Widgets.Welcome("bla bli blu", "bla"), "Welcome");
stack.set_visible_child_name("Welcome");
});
I do not know why it wont change the view. I specifically tell it to set the visible child to "Welcome" (and that is exactly how i named it one line above), but it just won't appear. Can someone explain to me why?
I can easily change the stack's visible child outside of the signal/event but inside of it won't do the trick..
Thanks a lot
Update: The issue was solved through José Fonte's comment down below: I instantiated the view, called the show_all () method on it and then added it to the stack and set the visible child to it.

Swap the type of link depending on model object

I'm at complete loss how to proceed further:
I have panel with a DropDownChoice and a submit button next to it. Depending on the selected value of the DropDownChoice (Obtained upon the firing of a OnChangeAjaxBehavior attached to it, the submit button needs to either replace the whole panel with a different one, OR become an ExternalLink.
Currently, the code looks like that:
public class ReportSelectionPanel extends Panel {
protected OptionItem selectedOption ;
public ReportSelectionPanel(String id) {
super(id);
IModel<List<OptionItem>> choices = new AbstractReadOnlyModel() {
// Create a list of options to be displayed in the DropDownChoice
} ;
final IModel<OptionItem> optionModel =
new PropertyModel<OptionItem>(this,"selectedOption") ;
final DropDownChoice<OptionItem> options =
new DropDownChoice("selectChoice",optionModel,choices) ;
// I don't know what the button should be... Plain Button? A Link?
final Component button = ???
options.add( new OnChangeAjaxBehavior() {
protected void onUpdate(AjaxRequestTarget target) {
if ( selectedOption.getChild() == null ) {
// button becomes an ExternalLink.
// A new window will popup once button is clicked
} else {
// button becomes a Something, and upon clicking,
// this ReportSelectionPanel instance gets replaced by
// an new Panel instance, the type of which is
// selectedOption.getChild()
}
} ) ;
I'm really not quite sure what the commented lines should become to achieve the result. Any suggestions?
Thanks!
Eric
IMHO it's nicer to keep just one button and just react differently depending on the selected option:
final Component button = new AjaxButton("button") {
public void onClick(AjaxRequestTarget target) {
if (selectedOption.getChild() == null) {
PopupSettings popup = new PopupSettings();
popup.setTarget("'" + externalUrl + "'");
target.appendJavascript(popup.getPopupJavaScript());
} else {
ReportSelectionPanel.this.replaceWith(new ReportResultPanel("..."));
}
}
};
// not needed if options and button are inside a form
// options.add( new OnChangeAjaxBehavior() { } ) ;

Create a BindableProperty depending on multiple properties

I'm trying to bind a UI element to different model properties A, B and AB. The first two properties A and B are controlled by two sliders. The third property AB is the sum of A and B. For each of the three properties there is a label displaying its value.
Now if I move one of the sliders, the corresponding label updates its Text. But the label for the combined property AB is not updated. Probably no "property changed" event is fired, since there is no setter for AB.
Is there any possibility for binding to such an "aggregated" property?
Here is the bindable object containing the properties A, B and AB:
public class Settings: BindableObject
{
public static readonly BindableProperty AProperty = BindableProperty.Create<Settings, double>(p => p.A, 0);
public static readonly BindableProperty BProperty = BindableProperty.Create<Settings, double>(p => p.B, 0);
public static readonly BindableProperty ABProperty = BindableProperty.Create<Settings, double>(p => p.AB, 0);
public double A {
get{ return (double)GetValue(AProperty); }
set{ SetValue(AProperty, (double)value); }
}
public double B {
get{ return (double)GetValue(BProperty); }
set{ SetValue(BProperty, (double)value); }
}
public double AB {
get{ return A + B; }
}
}
And here is the page containing both sliders and the three labels:
public class App : Application
{
public App()
{
var settings = new Settings();
var sliderA = new Slider();
sliderA.ValueChanged += (sender, e) => settings.A = e.NewValue;
var sliderB = new Slider();
sliderB.ValueChanged += (sender, e) => settings.B = e.NewValue;
var labelA = new Label{ BindingContext = settings };
labelA.SetBinding(Label.TextProperty, "A");
var labelB = new Label{ BindingContext = settings };
labelB.SetBinding(Label.TextProperty, "B");
var labelAB = new Label{ BindingContext = settings };
labelAB.SetBinding(Label.TextProperty, "AB");
MainPage = new ContentPage {
Content = new StackLayout {
VerticalOptions = LayoutOptions.Center,
Children = { sliderA, sliderB, labelA, labelB, labelAB },
},
};
}
}
This is what the running application looks like on iOS:
The last label should display the sum of the first two numbers.
Edit:
I wonder why I can't write
public static readonly BindableProperty ABProperty =
BindableProperty.Create<Settings, double>(p => p.A + p.B, 0);
But this yields the run-time error "System.TypeInitializationException: An exception was thrown by the type initializer for AggregatedBindablePropertyMnml.Settings ---> System.Exception: getter must be a MemberExpression"
Based on the suggestion from Taekahn (updating AB within the setters of A and B) I came up with the following solution.
By overriding the OnPropertyChanged method and setting the ABProperty, the bound label text is updated as well. In contrast to modifying each setter individually, this way we only need to modify the Settings class at one place.
protected override void OnPropertyChanged(string propertyName = null)
{
base.OnPropertyChanged(propertyName);
SetValue(ABProperty, A + B);
}
Now both sliders impact the third label:

Combo Box is not observed

I have the following Code:
public class GuiView extends Application {
private ObservableList<String> shareNames = FXCollections.observableArrayList();
public void start(Stage stage) {
...
ComboBox<String> comboBox = new ComboBox<String>();
comboBox.getItems().addAll(this.shareNames);
MenuItem open = new MenuItem("Open...");
open.setOnAction( e -> {
// FileChooser code...
if (selctedFile != null) {
this.shareNames.addAll("teststring");
}
});
}
}
When I run through the open dialog successfully the combo box doesn't update and shows the teststring. What is going wrong here?
You are updating shareNames, but that is not the list used by the combo box.
Either replace
comboBox.getItems().addAll(this.shareNames);
with
comboBox.setItems(this.shareNames);
or replace
this.shareNames.addAll("teststring");
with
comboBox.getItems().add("teststring");