Here's a minimal example to illustrate the problem. When the button is clicked, 500 TextView objects should get added, each containing some text. What actually happens is that there is a short delay, 500 empty TextViews get added, there is a much longer delay and then they all get populated with text at once and the layout sizes itself properly. Code below:
import gtk.Button;
import gtk.Main;
import gtk.MainWindow;
import gtk.Notebook;
import gtk.ScrolledWindow;
import gtk.Statusbar;
import gtk.TextView;
import gtk.TextBuffer;
import gtk.UIManager;
import gtk.VBox;
import gtk.Window;
import std.stdio;
class UI : MainWindow
{
Notebook notebook;
this() {
super("Test");
setDefaultSize(200, 100);
VBox box = new VBox(false, 2);
notebook = new Notebook();
Button button = new Button("add lines");
button.addOnClicked(&addLines);
box.packStart(notebook, true, true, 0);
box.packStart(button, false, false, 2);
add(box);
showAll();
}
void addLines(Button b) {
VBox box = new VBox(false, 2);
for (int i = 0; i < 500; i++) {
auto tv = new TextView();
tv.getBuffer().setText("line");
box.packStart(tv, false, false, 1);
}
ScrolledWindow swin = new ScrolledWindow(box);
notebook.add(swin);
showAll();
}
}
void main(string[] args)
{
Main.init(args);
auto ui = new UI();
Main.run();
}
Edit: this thread suggests that creating a bunch of text views is intrinsically expensive, and that I should be rewriting using a treeview.
GTK is event-driven, and uses a message pump. If in a callback you do a lengthy operation, you never give a chance to the message pump to process the pending messages. You could replace the code in your callback by a sleep of 2 seconds, the effect would be the same: the UI would be frozen during that time slice.
If you can't split up your actions, use the d equivalent of what is described in gtk_events_pending documentation:
/* computation going on */
...
while (gtk_events_pending ())
gtk_main_iteration ();
...
/* computation continued */
Called between each of your loop iterations, it will give some time to GTK to process the events you generated by adding your widgets.
After some more googling and experimenting, it turns out that GtkTextViews are intrinsically expensive to instantiate, and I should not have been trying to create so many of them. As per the advice in this thread I will be reworking my code to use a GtkTreeView instead.
Related
I have a Kotlin Multiplatform project where I want to use Events to control views.
The basic idea is this:
Buttons & Co fire an Event when clicked
These events get caught and handled by the responsible program components, which will in turn fire other events
Eventually, some kind of ViewEvent is fired, which is subscribed to by the ViewController
The ViewController then tells the program what should be drawn on the screen
In theory, that sounds like it should work. In practice, what happens is that while it gets to the point where the ViewController receives the event and reacts accordingly, the actual views are unaffected.
My ViewController looks like this:
import androidx.compose.runtime.Composable
import com.tri_tail.ceal_chronicler.events.OpenCharacterSelectionViewEvent
import com.tri_tail.ceal_chronicler.ui.main_view.MainView
import com.tri_tail.ceal_chronicler.ui.main_view.MainViewState
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
class ViewController {
private var mainViewState = MainViewState.TITLE
init {
val eventBus = EventBus.getDefault()
eventBus.register(this)
}
#Composable
fun draw() {
MainView(mainViewState)
}
#Subscribe
fun onOpenCharacterSelectionViewEvent(event: OpenCharacterSelectionViewEvent) {
mainViewState = MainViewState.CHARACTER
}
}
I debugged that, and was able to see that the mainViewState changes, as expected. However, the draw() function is never called again, and so the changed mainViewState never arrives in the MainView.
I've already tried making mainViewState a mutableStateOf(mainViewState), but that didn't change anything.
Furthermore, I can't just call draw() inside the onOpenCharacterSelectionViewEvent, because it is not #Composable, and adding that annotation to the method causes the build to fail.
At this point, I am not even sure whether what I am trying to do here can work this way. Can someone please help me out here?
I have also published a version of the code with the current non-working solution here: https://github.com/KiraResari/ceal-chronicler/tree/event-system
For my KMM project i use open source viewModel for KMM.
This one https://github.com/adeo-opensource/kviewmodel--mpp
I suggest transfer your MVC architecture to MVI and use this KMM viewModel to control your state as in usual android app.
Okay, so after worrying at this for several days, I have now come up with a solution that works.
Basically, the reason why it doesn't work as I tried it is that the frontend lives in its own little world, and it is very difficult for something from outside that world to affect it.
However, it can be done using delegates. Basically, what I called the ViewController in above is more of a MainViewModel, and it needs to look like this:
import com.tri_tail.ceal_chronicler.events.OpenCharacterSelectionViewEvent
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
class MainViewModel {
var state = MainViewState.TITLE
var updateState: ((MainViewState) -> Unit) = { }
set(value) {
field = value
updateState(state)
}
init {
val eventBus = EventBus.getDefault()
eventBus.register(this)
}
#Subscribe
fun onOpenCharacterSelectionViewEvent(event: OpenCharacterSelectionViewEvent) {
state = MainViewState.CHARACTER
updateState(state)
}
}
The other part of the magic happens in the MainView, where the state needs to be a remember with a mutableStateOf(..., policy = neverEqualPolicy()), and the delegate needs to be set like this:
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.*
import com.tri_tail.ceal_chronicler.models.main_view.MainViewModel
import com.tri_tail.ceal_chronicler.models.main_view.MainViewState
import com.tri_tail.ceal_chronicler.theme.AppTheme
import com.tri_tail.ceal_chronicler.ui.TitleScreen
import com.tri_tail.ceal_chronicler.ui.characters.DisplayCharacterSelector
#Composable
fun MainView(model: MainViewModel = MainViewModel()) {
var state by remember {
mutableStateOf(
model.state,
policy = neverEqualPolicy()
)
}
model.updateState = {
state = it
}
AppTheme {
when (state) {
MainViewState.TITLE -> TitleScreen()
MainViewState.CHARACTER -> DisplayCharacterSelector()
}
}
}
And that's all there is to it! Works like a charm, no extra libraries required.
I've made an aplication with vala where at some point I have to process a lot of files. I've created a window to choose a folder and then I get the paths of files and make some proces on them.
I've added a progress bar to this window to show how many files have been processed but for some reason it remains always empty.
Code about window:
this.files_window = new Gtk.Window();
this.files_window.window_position = Gtk.WindowPosition.CENTER;
this.files_window.destroy.connect (Gtk.main_quit);
// VBox:
Gtk.Box vbox = new Gtk.Box (Gtk.Orientation.VERTICAL, 5);
this.files_window.add (vbox);
// Buttons to open and close
Gtk.Button cancel = new Gtk.Button.with_label ("Cancel");
Gtk.Button select = new Gtk.Button.with_label ("Select");
vbox.add (select);
vbox.add (cancel);
// proogress bar
this.progress_bar = new Gtk.ProgressBar();
vbox.add(this.progress_bar);
// conect select to method do_stuff
select.clicked.connect (do_stuff);
this.files_window.show_all ();
As you can see, I connect the button "select" to the method "do_stuff" where I get the paths of selected files and make some process.
I update correctlly the fraction of the progres bar because I've added some prints to know if the value is correct and it is. It's just that the windows is not refreshing, possibly because all the work it is doing with the process of the files. Here is the code about do_stuff() method:
// some proces to get paths of files in the list sfiles
double fraction = 0.0;
this.progress_bar.set_fraction (fraction);
int processed_files = 0;
foreach (string sfile in sfiles) {
do_some_proces_to_file(sfile);
processed_files += 1;
fraction = (double)processed_files/(double)sfiles.length;
this.progress_bar.set_fraction (fraction);
stdout.printf("Real fraction: %f\n", this.progress_bar.get_fraction());
}
The printf shows that the value of the progres bar is being updated but in the window the bar is always empty.
Am I missing something? Is it the correct way to do the progres bar? Should I made another thread to do the stuff?
As #nemequ says, your code is blocking the main loop thread (which handles both user input and scheduling/drawing widget updates), hence it the progress bar is not updated until the method completes.
Using a thread is one way solve the problem, however using threads can lead to a lot of bugs however since it can be difficult to make even simple interactions between threads safe.
An async method avoids this by interleaving the code with the other work being done by the main loop. An async version of your do_stuff() would be pretty straight-forward to write, simply declare it async and put a yield in the for loop somewhere:
public async void do_stuff() {
...
foreach (string sfile in sfiles) {
// all of this is as before
do_some_proces_to_file(sfile);
processed_files += 1;
fraction = (double)processed_files/(double)sfiles.length;
this.progress_bar.set_fraction (fraction);
// Schedule the method to resume when idle, then
// yield control back to the caller
Idle.add(do_stuff.callback);
yield;
}
}
You can then kick it off from your click handler by calling: do_stuff.begin().
Unless there is some relevant code you're not showing, you're blocking the main loop. One option would be to do everything in a thread, and use an idle callback to update the UI. The basic idea is something like:
new GLib.Thread<void*>("file-processor", () => {
foreach (string sfile in sfiles) {
/* do stuff */
GLib.Idle.add(() => {
/* Update progress */
return false;
});
}
return null;
});
Depending on your application you may need to add a mutex to avoid race conditions. You may also need to add some logic for canceling the operation.
A better option might be to use a GLib.ThreadPool. You'd still want to update the UI from an idle callback, but this would allow each task to execute in parallel, which could provide a significant speed-up.
If I were you I'd probably wrap it all up in an async function to keep the API tidy, but you don't really have to.
I have written this Fantom class
using gfx
using fwt
class Test {
Window window := Window {
size = Size( 400, 320 )
SashPane {
Combo {
items = Month.vals
onModify.add( |e| { echo( "items.size is ${e->widget->items->size}" ) } )
},
},
}
Void main() {
window.open
}
}
when I run it, it produces this output:
items.size is 12
items.size is 12
which means that the modify event is being triggered twice. It occurs at the same time the window pops up on the screen, without me having any chance to modify anything on the Combo widget. Why?
This is causing issues in a real class that uses multiple Combo widgets, some of them related and causing a cascade of events that produces unexpected results.
Is there any way this can be prevented, please?
I can confirm that this is an issue.
Looking at the Java source code for the FWT Combo, it's pretty small and doesn't seem to do anything wrong, which leads me to believe that it's a issue with the SWT Combo Widget.
That doesn't help you any, so I had a quick play with the example and found this work around...
...add the onModify event listener after the window has been opened, and the widgets constructed. Do this by using the Window.onOpen() event:
using gfx
using fwt
class Testy {
Void main() {
Window {
size = Size( 400, 320 )
combo := null as Combo
onOpen.add {
combo.onModify.add { echo("Hello Mum!") }
}
SashPane {
combo = Combo { items = Month.vals },
},
}.open
}
}
Now you should only get a Hello Mum! when the combo is actually modified.
I am writing a program in Vala using GTK+. It has a function that creates a ListBox that contains a lot of EventBox objects. There is one issue: there is one function that downloads the image and it takes a lot of time, so the main window didn't show up unless all downloads are finished. This is not what I wanted, I wanted main window to appear and then images to download and to be shown. So I separated image load to separate function, but main window still doesn't show unless all downloads are finished. What am I doing wrong?
Here is the function I'm using:
foreach (MediaInfo post in feedPosts)
feedList.prepend(post);
foreach (PostBox box in feedList.boxes)
box.loadImage();
("feedList" is a class inherited from Gtk.ListBox and "boxes" is a list containing all of PostBox (which is inherited from Gtk.EventBox) objects)
This is feedList.prepend function:
public void append(MediaInfo post)
{
Gtk.Separator separator = new Gtk.Separator (Gtk.Orientation.HORIZONTAL);
base.prepend(separator);
PostBox box = new PostBox(post);
base.prepend(box);
boxes.append(box);
}
And this is the constructor and loadImage functions of PostBox class:
public PostBox(MediaInfo post)
{
box = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
this.add(box);
this.post = post;
userToolbar = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0);
userNameLabel = new Gtk.Label("#" + post.postedUser.username);
this.userNameLabel.set_markup(
"<span underline='none' font_weight='bold' size='large'>" +
post.postedUser.username + "</span>"
);
userToolbar.add(userNameLabel);
box.pack_start(userToolbar, false, true);
image = new Gtk.Image();
box.add(image);
box.add(new Gtk.Label(post.title));
box.add(new Gtk.Label( post.likesCount.to_string() + " likes."));
print("finished.\n");
return;
}
public void loadImage()
{
var imageFileName = PhotoStream.App.CACHE_URL + getFileName(post.image.url);
downloadFile(post.image.url, imageFileName);
Pixbuf imagePixbuf = new Pixbuf.from_file(imageFileName);
imagePixbuf = imagePixbuf.scale_simple(IMAGE_SIZE, IMAGE_SIZE, Gdk.InterpType.BILINEAR);
image.set_from_pixbuf(imagePixbuf);
}
You have written the download operations in another method, however the operations are still synchronous, i.e. they block the thread. You never want to do computationaly or otherwise expensive things in the GUI thread, because that makes the GUI unresponsive.
You should start your downloads asynchronously, and trigger a callback method when the download is complete. In the callback, then you may for example change the image placeholders to actual images.
I replaced all my async methods with multithreading and now it is working the way I want it to work.
Is there a way to keep listening to a property change, for a few seconds, then fire an event (call a method)?
For example, when the user enter data in a text field:
textField.textProperty().addListener(new ChangeListener<String>() {
#Override
public void changed(ObservableValue<? extends String> arg0, String arg1, String arg2) {
//before calling a method to do something.. wait for a few seconds ...
}
});
A scenario would be firing an action based on the string value. For example, hitting "M" for move, or "MA" for mask. I would like to "keep listening" for 2 seconds before making an action.
As Jeffrey pointed out, you can use ReactFX:
EventStreams.valuesOf(textField.textProperty())
.successionEnds(Duration.ofSeconds(2))
.subscribe(s -> doSomething(s));
There are a few ways to solve this.
Usually I would recommend the Java Timer API, but if by "making an action" you imply updating stuff on the FX thread, you would have to synchronize the threads, which is bothersome.
Depending on your use case, you could instead use Transitions or the Timeline in FX.
Here is an example with a transition:
package application;
import javafx.animation.RotateTransition;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.scene.text.FontPosture;
import javafx.scene.text.FontWeight;
import javafx.stage.Stage;
import javafx.util.Duration;
public class TransitionedInputExample extends Application {
#Override
public void start(final Stage stage) {
final ImageView spinner =
new ImageView("https://cdn1.iconfinder.com/data/icons/duesseldorf/16/process.png");
spinner.setVisible(false);
final Label title = new Label("Timed Action Commander Example", spinner);
title.setContentDisplay(ContentDisplay.BOTTOM);
title.setFont(Font.font("Helvetica", FontWeight.BOLD, FontPosture.REGULAR, 16));
final TextField textInput = new TextField();
textInput.setPromptText("Enter command");
final TextArea textOutput = new TextArea();
textOutput.setPromptText("Command results will show up here");
final VBox layout = new VBox(title, textInput, textOutput);
layout.setSpacing(24);
// setup some transition that rotates an icon for 2 seconds
final RotateTransition rotateTransition = new RotateTransition(Duration.seconds(1), spinner);
rotateTransition.setByAngle(90);
// delay rotation so that user can type without being distracted at once
rotateTransition.setDelay(Duration.seconds(1));
// restart transition on user input
textInput.textProperty().addListener((observable, oldText, newText) -> {
spinner.setVisible(true);
rotateTransition.playFromStart();
});
rotateTransition.setOnFinished((finishHim) -> {
// execute command
textOutput.setText("Executing " + textInput.getText());
spinner.setVisible(false);
});
final Scene scene = new Scene(layout);
stage.setScene(scene);
stage.show();
}
public static void main(final String[] args) {
launch(args);
}
}
For a solutition using a Timeline see this post .
You might consider using Inhibeans. It allows you to block the handling of an event until you want the change events to fire. The whole ReactFX project might also be useful because what essentially what you need to do is build a state machine of events. You are looking for patterns in the events like regex.
For example, let's say you use 'M' for move and 'MA' for mask. You'll have two paths through your state machine.
M
M -> A
Then you can use a timer to determine how long you'll wait for the events to pile up before you process it.
Checkout this sample copied from the ReactFX site:
reduceSuccessions
Accumulates events emitted in close temporal succession into one.
EventSource<Integer> source = new EventSource<>();
EventStream<Integer> accum = source.reduceSuccessions((a, b) -> a + b, Duration.ofMillis(200));
source.push(1);
source.push(2);
// wait 150ms
source.push(3);
// wait 150ms
source.push(4);
// wait 250ms
source.push(5);
// wait 250ms
In the above example, an event that is emitted no later than 200ms after the previous one is accumulated (added) to the previous one. accum emits these values: 10, 5.