qml

Rant : QML Horrors

I decided to make a Stock Photo Selector with QT Widgets.
Then, after quickly understanding how the QT Widgets system is old and a pain to deal with, when dealing with web resources, I decided to go the QML route.

The thing with QML is that you can quickly put an interface in place, add some code and then… When you try to architecture the whole program, things get complicated.

The idea

So, the whole concept of the Stock Photo Selector is to test Stock Photos on websites quickly and efficiently. There’s two objectives :

  • Be able to test the Stock Photos on PC and mobiles
  • Be able to save the right Stock Photo in the right folder of the website when done.

The first part can be done with simple Javascript, the second part however, not so much.

So, I went like “How about I create a search engine app with QT. It will search the photo. Once I double-click a photo, it sends it to the browser through a WebSocket, which then put it on the right place. Then when I got the right image, I just click ‘Save on server’ in the app and boom, done !“.

Sure, there’s still parts that are not clearly decided yet.
But I don’t care, because I want to test if the whole WebSocket communication is possible, in the first place.

Dealing with QML

QT Creator is almost useless with QML. Seriously.

The only thing that “kind of matter” is the Designer.
The IDE can really kick in when adding C++ helpers. But that’s it. For the rest, the IDE is USELESS. It provides almost no useful information, and can sometimes even provide idiotic completion or add things that are not understood by the QML parser.

I won’t even talk about the Designer crashing here and there, when the interface gets “too complicated”, and how the categorization of the wigets (?) make no sense most of the time.

In terms of “Debugging”, we’re far from In-Browser Javascript or Electron (kind of the same thing).

First, QML is an UI descriptive language. You can describe your UI in a human-readable manner. Something like this :

Window {
    id: window
    visible: true
    width: 640
    height: 480
    title: qsTr("Hello World")
    MyyToolbar {
        id: toolbar
        x: 0
        anchors.bottom: parent.bottom

        searchButton.onClicked: {
            var http = new XMLHttpRequest()
            var url = "https://pixabay.com/api/?image_type=photo&q=" + query.text + "&key=" + config.pixabay_key;
            http.open("GET", url, true);
            http.onreadystatechange = function() {
              if (http.readyState === 4) {
                  if (http.status === 200) {
                      results.addPixabayResults(JSON.parse(http.responseText))
                      console.debug(http.responseText);
                  }
                  else {
                      console.debug("XC")
                  }
              }
            }
            http.send();

        }
    }
}

However, trying to debug UI problems quickly become a pain in the ass, if you’re not accustomed to the weird QT Creator UI.

By default, the debugger is UNABLE to provide you a good view of the layout tree. If you click on “Pause” during the execution, you’ll see this :

>-app
|-argc
>-argv
>-engine
>-url

When I saw this I was like “??? So… ? app maybe ? Let’s see…”

v-app
|  >-[d]
|  >-[parent]
|  v-[children]
|  |  >-[0]
|  |  >-[1]
|  |  >-[2]
|  |  >-[3]
|  |  >-[4]
|  |  >-[5]
|  |  >-[6]
|  |  >-[7]
...

Then I was like “Whaaat ?”

I then discovered on the debugging view, you have your main code on the center, the locals on the right, and the stack trace pane below. In the toolbar of the stack trace pane, you have :

  • a menu Debugger ▼ which basically helps you swap this pane place with another pane
  • a menu GDB for “NameOfYourProject” ▼ which dictate which debugger you want to choose.

Turns out that, here, you want to click on GDB for “NameOfYourProject” ▼, then select QML for “NameOfYourProject” in the dropdown menu.
THEN, you’ll be able to see something that looks like this :

v-QQmlApplicationEngine
  >-Properties
  >-QTranslator
  >-QQmlFileSelector
  >-QQuickIcon
  >-QQuickFontValueType
  >-QQuickPalette
  >-window

If you expand the window list, you’ll see all the children of your view, with all its properties and such.

Which brings my first question :

WHY IS QML NOT SELECTED BY DEFAULT, DURING QML APPLICATION DEBUGGING !?

I hate the whole “Where’s Wally” UI game. Having to specifically click ONE element of a pane which seems to only dispay stack traces (with thread selection, step by step debugging, assembler debugging, …), in order to see my UI elements is madness.
I kind of understand the mental process that went behind it “Well, to select which locals you want to display, you’ll have to select the right thread in the stack trace pane, so we’ll put the menu there so that they can switch between C++ locals and QML/Javascript locals” but… ugh… it’s still unintuitive.

I’d highly prefer a tool that’s specifically oriented towards UI debugging.
If you programmed with Android, you know what I’m talking about.
I’m talking about a panel where you can see all the currently instantiated UI elements.
Following the previous example, such a panel would look like this :

Window
	⌞ MyyToolbar
		⌞ TextField
		⌞ ComboBox
		⌞ Button

Then when you click on an element, it selects it in the program and you can view its properties. Like its position, width, height, z-index, …

That kind of thing helps TREMENDOUSLY in trying to determine why something is not displayed correctly.

But well, with QT Creator, you don’t have this.

But wait, there’s more !

Dimensions hell

So, in my app, I was able to display the thumbnails of the Pixabay’s queries results in a Grid, and I was able to trigger the download of a decent resolution version of a photo when clicking on it…

“Wow, you were able to display images in a Grid and add an OnClickListener ! Must have been really hard ! /s”

Well, YES ! IT WAS !

Due to some to mystic positioning and size rules on the Image widget, I lost an entire day figuring out how to add an “Click” listener on a freaking Image widget !

By default, in my case, the Image widget would start with no width and height, because it changes based on the width and height of the thumbnail downloaded.

So, when adding a Mouse Area as a child, the MouseArea starts with a width and height of 0.
And even with anchors.fill, changing the width and height properties of Image will NOT change the properties of the MouseArea child. Because… REASONS !

Trying to put a height/width on the Image on start also didn’t work.

However ! When doing something like this :

Rectangle {
	Image { anchors.fill: parent }
	MouseArea { anchors.fill: parent }
}

It works !

Turns out that updating the Rectangle dimensions, when displaying the thumbnail in Image, makes the MouseArea redimension itself correctly and the whole things start working.

YAY ! I just lost an entire day !

Broken WebSocket and useless Javascript debugger

So, the pictures are displayed correctly (but are still going over the toolbar for reasons I don’t get !… ugh… z-index maybe ?), I can click on them :
GOOD !

Now, what’s remaining is downloading them and sending them through WebSocket.

The setup of the WebSocket server was impressively easy ! Almost no fuss at all. It just worked as intended.

So, I found a way to save the WebSocket object provided during the client connection. Then, when clicking on a thumbnail, I triggered an HTTP Request with a good old XMLHttpRequest object, and when receiving the response, I did :

ws.sendBinaryMessage(http.response)

On the browser side, I first tried to rush and do something like this :

if (message.constructor === Blob) {
	var reader = new FileReader();
	reader.readAsDataURL(message);
	reader.onloadend = function() {
		var base64data = reader.result.substring("data:application/octet-stream;base64,".length);
		console.log(base64data);
		const mimed_data = "url('data:image/jpg;base64," + base64data + "')";
		document.querySelector("#wss_image").style.backgroundImage = mimed_data;
	}
}

And… it didn’t work…

Hmm… I tried to copy the base64 to a file and convert it back, to see how it went.

Apparté

Now, let me just say how most of today apps can’t deal with large block of text. KDE apps being on the worst offender side.

In Firefox, the UI was almost to a crawl when trying to select the large block of Base64 text representing the picture.

Seriously, I tried to save it in Kate : Nope. I tried to save it in KWrite : Nope. Then, I launched notepad through Wine. YES, seriously !

IT WORKED !

But it was sluggish as hell…

Then I brought emacs and did the operation in 3 seconds.

What’s the spec of the machine doing this already ? Hmm ?
Oh, it’s just an 8 core / 16 threads AMD Ryzen 7 2700X with 32GB of RAM and NVMe disks. So it’s normal if it can’t copy 2MB of text…

KDE apps are so well optimised.

Back on track

I saved the base64 block in a text file, decoded it with a simple Ruby script :

require "base64"
File.write("decoded.jpg", Base64.decode64(File.read("encoded_image.txt")))

Then downloaded the actual picture on Firefox, saved it and compared the size… ALMOST TWICE THE SIZE !?… WHAT ?

Ok…

Can I see the headers ?

ef bf bd ef bf bd ef bf bd ef bf bd 00 10 4a 46 49 46

4a 46 49 46 is JFIF. But what’s that ef bf bd thing ?

Let’s compare it with a valid JPG header.

ff d8 ff e0 00 10 4a 46 49 46

… ??? Okay ??? Where’s the ff d8 ff sequence in the converted version ? Did it convert it ?

If I do a websearch for ef bf bd ef, maybe I’ll get a clue about what’s going on…

… It’s an UTF-8 to Latin-1 decoding issue … !!?

YOU’RE FUCKING KIDDING ME ?

I TELL THE FUCKING SOFTWARE TO SEND EVERYTHING IN BINARY, AND IT DOES AN UTF-8 TO LATIN-1 CONVERSION ON BINARY DATA !?

WHAAT ?

Wait, maybe that’s Firefox ? Maybe I didn’t setup the whole <meta charset="utf8"> correctly and it starts to convert data.

Let’s bring Wireshark, listen on lo, redo the whole operation;

NO, OKAY, IT’S QML !

QML, WHAT THE FUCK ! WHEN I SEND A BINARY MESSAGE, IT’S A BINARY MESSAGE ! DON’T TOUCH IT ! JUST SEND THE DATA ‘AS-IS’ !

For fuck sake, this reminds me the whole bullshit that reading files is on Windows. If you don’t open files in “binary” mode on Windows, lonely \x0d and \x0a are automatically converted into \x0d\x0a sequences, for archaic Mac OS and UNIX compatability reasons, botching up binary files to no end.

Let’s bring up the debugger

Back to the QML code, I put a breakpoint just below if (http.readyState === 4 && http.status === 200) { in the onreadystatechange callback of the ‘XMLHttpRequest’ used for the request.

Basically, it looks like this :

http.onreadystatechange = function() {
	if (http.readyState === 4 && http.status === 200) {
		ws.sendBinaryMessage(http.response);
		console.debug(typeof(http.response))
		console.debug(http.response.length);
		console.debug(http.responseText.length);
		console.debug("Got : " + picture.data["largeImageURL"]);
	}
}

You’d be surprised to know that the size provided by http.response.length and http.responseText.length are identical and they also also seem wrong.
They’re down a few kilo-bytes compared to the original image…
I don’t know why.

But it’s way different from the size of the element sent via sendBinaryMessage which inflates the size by almost 2 times.

So I put a breakpoint, run the code with the debugger, the debugger stops and in the locals, all I can see is this :

|-this
v-http
|  ⌞onreadystatechange
|-onClicked
v-mouse
	⌞ ...

So this is empty. http has one field onreadystatechange and… that’s it. onClicked is empty and mouse has fields corresponding the pointer properties when clicking the area.

The locals pane is USELESS to me, here.
Sure I can test a few things in the QML console, but that doesn’t help me that much.

Ok, how about Step into, maybe I can see the code of that sendBinaryMessage thing… it just jumped onto the next console.debug line…

How stupid this thing can be ?

Switching back to GDB for … shows nothing…

Ugh… so I have zero idea about :

  • Why did it do a conversion ?
  • How to prevent it ?

I LOVE to code with QML ?

The idea now is to add some C++ helpers, hoping that the C++ part is not AS broken AS the Javascript part.
Worse part, I’ll have to resort to an external WebSocket C/C++ library, use it in C++ helpers and hope that it does the trick.