I Use This!
Activity Not Available

News

Analyzed 4 months ago. based on code collected 9 months ago.
Posted about 2 years ago by Mellow Clamour | flyingcakes - planet-kde
DISCLAIMER : This post is entirely my personal view, and does not reflect that of any projects I contribute(d) to. Hey! In this post I've shared my personal experience with contributing to open source projects. I'm still a learner, and probably ... [More] will be one for rest of my life. But I believe I'm beyond that point where I was confused about "how do I even start". I'm recording my experience in hope that it proves to be useful to someone else. Since this post is meant to record my personal experience and observations, I have glossed over some commonly known practices like looking for good first issues, or learning basics of the toolchain you'll be using. I find those advice to be abundantly available on the internet. Be inspired The first step is pretty simple. Be inspired! As simple as it sounds, many people don't know exactly why they should be contributing to open source projects. Do you want to improve application reliability for thousands (or even millions) of users around the world? Do you think you can add a cool feature to your favorite application? Do you believe that you can lend a helping hand to the developers pushing updates to applications you use daily? (and many of them are probably doing it without any pay) If your answer to any of the above is "yes", you've already completed the most difficult step. Finding true love If your answer was "no", well, how do you find that application or organization where you're truly motivated to contribute? First step wolud be start using more open source software. As you increase your exposure with open source software, you'll eventually find a software that you really want to help with and improve upon. I really suggest switching to a Linux distribution at this point, because that will make your life much easier. My story, you ask? I have tried contributing to open source projects here and there, but the lack of real motivation was a big friction point for me. Fast-forward to April 2020, when I first used Endeavour OS. I found it to be a great distribution, offering the experience I really liked and believed in. Months later, I realized I really wanted to help them out. In whatever small way I can. February 2022 marks one year of me being on the team. If you try contributing to a software you use, it will be much easier for you to test changes and come up with new ideas/ improvements. But, I don't know coding :( "I don't know coding, how do I contribute?" This is a big misconception among people new to the open source scene. Programming is just one part of the process. There are many other things a project needs help with. Graphics, translation, cloud infrastructure maintenance, promotions, testing, support... KDE even had openings for music producers willing to work on sound design for Plasma! Some of these don't even require a very high technical IQ. Take for example testing - an average Joe is as suited to test applications as the project developer, since most of the users are going to be the average Joe. On Endeavour OS, I do testing, theming, packaging etc. Any one who can write code to traverse an array can certainly do these things. So you can alreayd guess there is no coding involved. But my favorite work is seeing those experienced people discuss problems and find solutions. That is what grows me as a learner. However, that doesn't get me into XYZ open source event "Most open source events for students are geared towards programming, so how do I make it if I contribute with other stuff you just mentioned above?" If you've reached here, I suppose you said "yes" to the questions I put up in the first section. If you're doing this only to get into an open source event - that would totally defeat the purpose. Yes, it's good to try getting into these, but in case you're not able to make it, you shouldn't stop contributing to open source altogether. You can still contribute even if you're not a part of any student event. What should motivate you is the fact that you'll be improving user experience for thousands (or millions) around the world. Even if you don't contribute code, there is a lot to learn. Just by seeing how the project members function, you get an idea about managing large communities. You get better with your communication skills. You learn more about the organization and make friends, who could certainly help or guide you. (seriously, knowing an experienced person is way better than a hundred unknown LinkedIn connections) Ok so, how do I start? You've finally found the project(s) you want to work on. What do you do now? First up, join whatever communication platform the project uses. Matrix, IRC, forum and mailing lists are the common ones. Introduce yourself - keep it short and to the point. List your technical skills that are relevant to the project. You don't go to a kernel development group and say that you know React. (unless you're discussing their website) Most likely, you're going to be ignored. It shows that you did not try to read up what the project is about and how they do their work. Introducing yourself is - undoubtedly - one of the most dreaded questions. Looking back, I find that my non-coding days at Endeavour OS taught me a lot about how open source projects function. Being a mini maintainer myself, I have seen quite a few potential contributors. I know what a project maintainer would like to hear. And that helps me when I am the one introducing myself in communities where I'm new. From the last section - even if you're not programming for a project, you're still progressing faster than you know. Stalking communication channels, I've seen a couple of copy-paste introductions and project ideas. The posters had put in absolutely zero effort in even looking up what kind of products does organization makes. As a result, they either were gracefully ignored, or received a feeble response. So, do little bit of research beforehand. Do your ideas suit the project goals and philosophy? It is much easier and smoother working on a project where you believe in the goals and philosophy. Once you have introduced yourself, a community member should point you towards the resources and relevant communication channels where you could help out. Do note, exactly nobody will spoon-feed you. Most likely, you'll get link to a wiki page, or link to a different group. It's your task to explore and follow the links to find what you want to help with. Say, you want to join the promo team. Of course, you'll first need to join their channel. For a couple of days, try to just observe how they work. They are probably more efficient than the way you're used to working with your friends. Try to understand the tone and air of the channel. (you never want to crack an out-of-place joke) Once you feel you understand their flow, start chipping in with your suggestions, ideas and opinions. Again, you're probably not contributing something tangible at the beginning, but one idea leads to another - you never know when your simple idea transforms into a lengthy discussion and ends up as the next big thing. And obviously, never send memes in #general XD If you actually want to get into coding... It is a tough ride ahead, but I promise, it is very rewarding. As I said earlier, if you are contributing to a software you already use, chances are you already have some suggestions or ideas to make it better. Why not take them as the starting point? Before starting to work on anything, it is always good to discuss with other team members about the changes. It is possible that your suggestion goes against the project philosophy, so it might need some tweaking. Or in the other case, an experienced member could help you refine your idea. Also, make it a point to look up if the idea or bug you want to work on has already been reported and/or is being worked upon. If yes, you could team up with the person(s) working on it, and help them with testing, or giving ideas. What if you don't have any idea, or you're new to the project altogether? Download it and start using! Try to look around the application to get a sense of what is it about. Don't straight dive into the code. Look for pending issues. Try to assess which issues you could possibly solve. You don't have to solve the complete issue in your head right now. Just think about what all tools you'll need. Do you know how to use them? If still looks difficult at this point, simply try to reproduce the bug. There are many nasty bugs that are hard to reproduce. Even if you come up with a definitive process to reproduce the bug, you'll be doing a great help to whoever takes up the task of fixing it. Share your findings with the team, and follow the discussion. When the bug is finally patched, try reading the diffs and see how it was solved. This will give you idea about at least one component of the application. If submitting patches, feel free to discuss or take help from the community members. Most likely they'll help you out. Never be afraid of code reviews. See them as light discussions. My first code patch was not merged, and the discussion only led me to contributing a better patch. I've recorded the experience below. My experience The first "coding" related issue I had to fix on a KDE project was on KDF. You can see my bug report here. KDF was using Dolphin as the default file manager, instead of the system default. It did let users change file manager from settings, but that wouldn't work when using KDF as a Flatpak. So, in my first patch, I removed the feature of selecting file manager altogether and made it such that KDF will always use system default file manager. In review, I was suggested to not remove the feature, but instead give users the option to either use default file manager, or specify a custom one. I closed the pull request and the next day, created a new one, which was finally merged. My code contribution wasn't something too significant, but it helped me become more confident with QtWidgets, C++, signals - slots and by poking around in the source code, I also learned how the project is structured and how they use their framework to store user preferences. Tiny patch, but big learning. As my next patch, I was suggested to add a feature that would detect if application is running as a Flatpak, and disable the custom file manager input automatically. This time I already knew about the application. I sent the patch rather quickly. Of course, I got a lot of help from the community members. The whole experience makes it worth it for me to contribute. Going further By now, you probably have some idea about how to go about making your initial contributions to your favorite project. Once you make your first successful contribution, you feel lot more confident. You feel motivated to take on bigger tasks, and become active in discussions. You should have already realized that this is a team effort. You work with constant back and forth reviews and suggestions. The more you participate, the more you learn. Probably for this reason, I like joining different channels and stalking their discussions. (even if I don't understand half the technical terms they use!) This was a beginner sharing his experience for other beginners. Hoping this post cleared up your doubts and you feel more confident about stepping into the world of open source software development! [Less]
Posted about 2 years ago by KDE Community
Over 120 individual programs plus dozens of programmer libraries and feature plugins are released simultaneously as part of KDE Gear. Today they all get new bugfix source releases with updated translations. Distro and app store packagers should ... [More] update their application packages. 21.12 release notes for information on tarballs and known issues. Package download wiki page 21.12.2 full changelog [Less]
Posted about 2 years ago by KDAB on Qt
Writing mobile apps can be a lot of fun, especially with Qt and QML. But if your background is all about C++ and Qt, writing the entire required stack for a mobile app can be challenging. Adding push notifications, storing information, and having to ... [More] write that with another language and framework might make you give up. But we got you covered! This is going to be a series of tutorials on how to build a simple mobile chat application with push notifications. On the server side, it will will have a REST API powered by Cutelyst, featuring a database with async communication for storing conversations. Additionally, it will communicate with Firebase to send the notifications. As you might have already noticed, we are using Qt everywhere (hence, Qt Allstack). The client application will try to register a nickname. Then, it will connect to a global chat room. For the purpose of simplicity, it’s going to be a single chat room. Once someone sends a message with @some_nick within the string, the push notification will go only to that user app. Thanks to PostgreSQL IPC notifications, we get it in real time. Setup I’ll list here what I’m using on this project, but you can also adjust it to your OS or distro: Kubuntu 21.04 PostgreSQL 13.3 Qt 5.15 from the distro for the backend and Qt 5.15 from online installer for the mobile app to have Qt libraries for Android Cutelyst 3.1 ASql 0.50 CMake 3.18 Prebuilt packages for Cutelyst and ASql are available for download. Backend We will start by creating the backend: cutelyst3-qt5 --create-app KDChatAppBack This creates a bare minimum project to start with. Let’s compile it to see if it runs: cd KDChatAppBack/build && cmake .. && make If everything compiled correctly, we can run it with: cutelyst3-qt5 --server --restart --app-file src/libKDChatAppBack The –sever option starts a Cutelyst server listening by default on all addresses at port 3000. –restart will keep an eye on the Cutelyst application and restart the server when it changes, and –app-file specifies where our application file is located. Open QtCreator and “Open New Project”, selecting the CMakeLists.txt file. Then, on the “Configure” step, choose the “build” directory in which we compiled the application. This way, QtCreator can compile in the same directory. Now let’s add ASql dependency. In ChatAppBack/CMakeLists.txt, we add after find_package: find_package(ASqlQt5 0.43 REQUIRED) Then on src/CMakeLists.txt, make sure our target links to ASql as well: target_link_libraries(ChatAppBack Cutelyst::Core ASqlQt5::Core # link to ASql ASqlQt5::Pg # link to ASql Postgres driver Qt5::Core Qt5::Network ) We are going to use the PostgreSQL database, as it’s very easy to get started, has great JSON support and, most importantly, it has IPC built-in — a critical feature for a real-time chat that even commercial players lack. In order to have the nicest development environment, it’s recommended to create a database user with the same name as your machine login name, and grant it rights to create new databases. That can be done with this command: sudo -u postgres createuser --createdb $USER We can now create and manipulate databases without needing a password or changing configuration files. Just issue the following command as your regular user and we will have a new database to use: createdb chat ASql comes with a handy tool called migrations; we will use it to have our database versioned. On QtCreator, click to create a New File → Template “General” → “Empty File”. Name the new file db.sql at ChatAppBack/root, with the following content: -- 1 up CREATE TABLE users ( id serial PRIMARY KEY, nick text NOT NULL UNIQUE, data jsonb ); -- 1 down DROP TABLE users; -- 2 up CREATE TABLE messages ( id serial PRIMARY KEY, created_at timestamp with time zone DEFAULT now(), user_id integer NOT NULL REFERENCES users(id), msg text NOT NULL ); -- 2 down DROP TABLE message; -- 3 up CREATE OR REPLACE FUNCTION messages_notify() RETURNS trigger AS $$ BEGIN PERFORM pg_notify('new_message', json_build_object('id', NEW.id, 'msg', NEW.msg, 'nick', nick, 'created_at', NEW.created_at)::text) FROM users WHERE id = NEW.user_id; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER messages_notify AFTER INSERT ON messages FOR EACH ROW EXECUTE PROCEDURE messages_notify(); -- 3 down DROP FUNCTION messages_notify(); As you can see, there are 6 scripts. When version goes from 0 to 3, all up scripts are executed. If you want to rollback a migration, it uses the down scripts. The scripts create a users table with a unique nick and some extra JSON metadata, then a messages table with each message, the user_id that sent the message, and the time the message was created. Script 3 creates a trigger that will notify all front-end servers (this way we can scale our simple app horizontally) that a new message is available. ASql migrations can be used from the command line or by writing C++ code that makes use of its class. It’s easier to just issue it: asql-migration0-qt5 --connection postgres:///chat --name chat root/db.sql This will create a table called asql_migrations and track the version under the name “chat”. You can take a look at your tables with: psql chat chat=> \d users Now let’s configure our backend to create a pool of database connections. We will override to postFork(). This method is called for each thread or process that is created. An ASql pool has thread affinity with the thread that created the pool, add in chatapp.h: bool postFork() override; Now add this implementation on the chatapp.cpp: #include #include bool ChatAppBack::postFork() { APool::create(APg::factory("postgres:///chat")); return true; } Compile and make sure the application restarts properly. REST API The REST API will be responsible for creating a new user and posting new messages. We won’t worry about authentication or nice URL APIs. Adding a JWT and properly naming methods is left as an exercise to the reader. So in root.h, we will add: C_ATTR(users, :Local :AutoArgs :ActionClass(REST)) void users(Context *c) {}; C_ATTR(users_POST, :Private) void users_POST(Context *c); C_ATTR(users_PUT, :Private) void users_PUT(Context *c); C_ATTR(messages, :Local :AutoArgs :ActionClass(REST)) void messages(Context *c) {}; C_ATTR(messages_POST, :Private) void messages_POST(Context *c); The first :Local method will create a /users URL end point, since there are no more arguments after Context*. :AutoArgs will know the URL can’t have additional arguments. The special action class, REST, will take care of issuing the methods. It will look for users_METHOD. Once a new HTTP request arrives, it will know to which method to call, In our case, we are only accepting POST and PUT methods. So if you try to GET/DELETE, it will fail but it will respond with the proper reply if the OPTIONS method is requested. The same applies to messages end point (ie /messages accepting POSTs only). Now that Cutelyst knows how to route HTTP requests, it’s time to write real code in root.cpp: #include #include #include #include void Root::users_POST(Context *c) { const QJsonObject data = c->request()->bodyJsonObject(); ASync a(c); APool::database().exec(u"INSERT INTO users (nick, data) VALUES ($1, $2) RETURNING id", { data["nick"], data, }, [a, c] (AResult &result) { auto firstRow = result.begin(); if (!result.error() && firstRow != result.end()) { // RETURN the new user ID c->res()->setJsonObjectBody({ {"id", firstRow[0].toInt()}, }); } else { qWarning() << "Failed to create user" << result.errorString(); c->res()->setStatus(Response::InternalServerError); c->res()->setJsonObjectBody({ {"error_msg", "failed to create user"}, }); } }, c); } When creating a user, we first get the JSON sent by the client. Then, we create a scoped ASync object that must be explicitly captured by the lambda. It’s responsible for telling Cutelyst that it should not send a reply for the client immediately. It will do so when the last ASync object goes out of scope. Then, a ADatabase object is retrieved from pool, which we’ll then call exec(). Notice that we pass a QJsonObject to the database and ASql properly handles it. After the user is created, we get an ID and will use it on our app to send messages. That’s not safe! I know: void Root::users_PUT(Context *c) { const QJsonObject data = c->request()->bodyJsonObject(); ASync a(c); APool::database().exec(u"UPDATE users SET nick=$1, data=$2 WHERE id=$3", { data["nick"], data, data["user_id"], }, [a, c, data] (AResult &result) { if (!result.error() && result.numRowsAffected()) { c->res()->setJsonObjectBody({ {"id", data["user_id"]}, }); } else { qWarning() << "Failed to create user" << result.errorString(); c->res()->setStatus(Response::InternalServerError); c->res()->setJsonObjectBody({ {"error_msg", "failed to create user"}, }); } }, c); } The PUT method is about updating our already-created user. Later, we can use this to send the Firebase token that the server will need to send push notifications. void Root::messages_POST(Context *c) { const QJsonObject data = c->request()->bodyJsonObject(); const QString msg = data["msg"].toString(); ASync a(c); APool::database().exec(u"INSERT INTO messages (user_id, msg) VALUES ($1, $2) RETURNING id", { data["user_id"], msg, }, [a, c, msg] (AResult &result) { auto firstRow = result.begin(); if (!result.error() && firstRow != result.end()) { // RETURN the new message ID c->res()->setJsonObjectBody({ {"id", firstRow[0].toInt()}, }); } else { qWarning() << "Failed to create message" << result.errorString(); c->res()->setStatus(Response::InternalServerError); c->res()->setJsonObjectBody({ {"error_msg", "failed to create message"}, }); } }, c); } The message’s POST method is very similar, except we select which values we want from the JSON object. For a careful observer, it’s passing a QJsonValue and not an int or QString. This method also returns the new message ID. This way, our client can ignore its own message when it gets notified. Websockets An important part of any chat application is working in real-time. We can do this with WebSockets. The client app will stay connected to receive new messages. As soon as the database says there is a new one, we send it to the connected clients. In root.h, we add: C_ATTR(websocket, :Path('ws') :AutoArgs) void websocket(Context *c, const QString &user_id); QHash m_wsClients; The :Path(‘ws’) tells Cutelyst to call this method when the URL is /ws, even though the method name is websocket. :AutoArgs now sees the QString argument. So, the end point is /ws/. Finally, QHash will keep a pointer to the Context objects, using their user_id as key. void Root::websocket(Context *c, const QString &user_id) { if (!c->response()->webSocketHandshake()) { c->response()->webSocketClose(Response::CloseCodeNormal, QStringLiteral("internal-server-error")); return; } if (m_wsClients.contains(user_id.toInt())) { c->response()->webSocketClose(Response::CloseCodeNormal, QStringLiteral("already-logged-in")); return; } m_wsClients.insert(user_id.toInt(), c); connect(c, &Context::destroyed, this, [=] { m_wsClients.remove(user_id.toInt()); }); APool::database().exec(uR"V0G0N( SELECT m.id, m.created_at, u.nick, m.msg FROM messages m INNER JOIN users u ON m.user_id=u.id ORDER BY 2 DESC )V0G0N", [c] (AResult &result) { if (result.error()) { c->response()->webSocketClose(Response::CloseCodeNormal, QStringLiteral("error-getting-msgs")); } else { c->response()->webSocketTextMessage(QJsonDocument(result.jsonArray()).toJson()); } }, c); } Our new method needs to change the protocol HTTP → WebSocket. Then, it checks if the user is connected and drops the connection when logged in. It also removes the client connection when the client goes away. Last, it gets all server messages. Notice that the ASync class is not needed here. The WebSocket protocol is async by definition. So, now you need to work on its signals. Another important point here is that the last parameter of ADatabase::exec() is a QObject pointer. This allows for cancellation of the query when the object gets destroyed, as well as not calling this lambda at all, which would crash if called with an invalid Context pointer. Postgres Notifications Now we need to listen for database notifications, which should sound when a new message is added. For this, we will need a dedicated DB connection for monitoring for states changes, so our clients don’t miss a notification due a broken connection with the database. On root.h, we will add a postFork() override. We won’t use the other one, as we need access to the QHash containing our connected clients. So, it’s easier to add it on root.h: bool postFork(Application *app) override; Notice the controller’s postFork() take an application pointer, then add the following code on root.cpp: bool Root::postFork(Application *app) { auto db = APool::database(); auto subscribe = [=] () mutable { db.subscribeToNotification("new_message", [=] (const ADatabaseNotification &notification) { for (const auto &ws : qAsConst(m_wsClients)) { ws->response()->webSocketTextMessage(notification.payload.toString()); } }, this); }; db.onStateChanged([=] (ADatabase::State state, const QString &msg) mutable { if (state == ADatabase::State::Disconnected) { qCritical() << "DB connection closed, disconnecting clients"; for (const auto &ws : qAsConst(m_wsClients)) { ws->response()->webSocketClose(Response::CloseCodeNormal, "db-disconnected"); } } else if (state == ADatabase::State::Connected) { subscribe(); } }); return true; } Here, we create a named lambda to subscribe to “new_message” notification. We do this in a lambda because, if the connection is closed, the subscription will be lost. So, we’d need to subscribe again. Once we get a new message, we send it to all of our connected clients. If the database connections is lost, we close the connection with all our clients. Notice that we don’t release their pointers from m_wsClients because that will happen with the code added on the WebSocket connection. The backend code now only misses sending a notification with Firebase, which will be added later. Client App The client app now needs to talk to our backend server. It’s going to be a simple stack application with material design. When it’s done, we will be adding support for push notifications. The code that I currently have working with this was created on top of QMake. So to avoid issues, we won’t be using CMake for the mobile app. Create a new project in Qt Creator. I always open a new Qt Creator, as switching between projects is slower than switching windows. Choose “Application (Qt Quick)” → “Qt Quick Application – Empty”, then name it “ChatApp” and choose qmake build system. Since we chose the empty template, we need to manually define the Qt Quick Controls 2 theme. To do so, right click on the qml.rc resource and choose “Add new…” → “General” → “Empty file”, then name it qtquickcontrols2.conf and add the following content: [Controls] Style=Material We also need to set the application and organization values, so we can store settings. Add the following to main.cpp: QCoreApplication::setOrganizationDomain("com.kdab"); QCoreApplication::setOrganizationName("kdab"); QCoreApplication::setApplicationName("ChatApp"); The client app will have only 3 QML files, 2 of which you can now create with Qt Creator as Qt → QML File named PageUser.qml and PageMessages.qml. The third one will be main.qml, which already exists. We’ll start with the main.qml code, which will be like this: import QtQuick 2.12 import QtQuick.Controls 2.5 import Qt.labs.settings 1.0 ApplicationWindow { id: window width: 640 height: 480 visible: true title: qsTr("ChatApp") header: ToolBar { contentHeight: toolButton.implicitHeight ToolButton { id: toolButton visible: settings.user_id !== 0 text: stackView.depth > 1 ? "\u25C0" : "\u2630" font.pixelSize: Qt.application.font.pixelSize * 1.6 onClicked: { if (stackView.depth > 1) { stackView.pop() } else { drawer.open() } } } Label { text: stackView.currentItem.title anchors.centerIn: parent } } Settings { id: settings property int user_id: 0 property string server property string nick property string fullname } Drawer { id: drawer width: window.width * 0.66 height: window.height Column { anchors.fill: parent ItemDelegate { text: qsTr("Edit User") width: parent.width enabled: settings.user_id !== 0 onClicked: { stackView.push("PageUser.qml") drawer.close() } } } } StackView { id: stackView initialItem: "PageMessages.qml" anchors.fill: parent } Component.onCompleted: { if (settings.user_id === 0) { stackView.push("PageUser.qml") } } } Here we have a Settings object to hold our registered user_id, which defaults to zero when not registered, our server, nick, and fullname. When the app starts, if not registered, it shows the PageUser.qml for registration. Once registered, we will see PageMessages. For PageUser, we will have: import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import com.kdab 1.0 Page { title: settings.user_id === 0 ? "Create User" : "Update User" ColumnLayout { anchors.fill: parent anchors.margins: 10 Label { text: "Server:Port" } TextField { Layout.fillWidth: true id: serverF text: settings.server } Label { text: "Nick" } TextField { Layout.fillWidth: true id: nickF text: settings.nick } Label { text: "Full Name" } TextField { Layout.fillWidth: true id: fullnameF text: settings.fullname } Button { text: settings.user_id === 0 ? "Create" : "Update" onClicked: { var nick = nickF.text var fullname = fullnameF.text var server = serverF.text var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (xhr.readyState === XMLHttpRequest.DONE) { if (xhr.status === 200) { var json = JSON.parse(xhr.responseText) settings.user_id = json.id settings.nick = nick settings.fullname = fullname settings.server = server stackView.pop() } else { console.error("Error creating/updating user: ", xhr.statusText) } } } xhr.open(settings.user_id === 0 ? "POST" : "PUT", "http://" + server + "/users"); xhr.setRequestHeader("Content-Type", "application/json"); xhr.send(JSON.stringify({ user_id: settings.user_id, nick: nick, fullname: fullname })); } } Item { Layout.fillHeight: true } } } The important part is the Button::clicked(). It will create a XMLHttpRequest that will POST or PUT, depending on whether we have a user_id. Set the proper Content-Type and send the JSON object with our data. The XMLHttpRequest.onreadystatechange callback will store the returned ID on our Settings object, as well as both nick and fullname. The server string is also very important, as when testing on the mobile phone you will want to set it to your machine IP:Port instead of localhost:3000. Now that we have a user_id, we are allowed to leave the PageUser and finally see the chat room. PageMessages.qml will feature the following code: import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import QtWebSockets 1.1 import QtQuick.Controls.Material 2.12 import QtQml 2.12 Page { title: "Messages" ListModel { id: messages } WebSocket { id: ws url: "ws://" + settings.server + "/ws/" + settings.user_id onTextMessageReceived: { var json = JSON.parse(message) if (Array.isArray(json)) { json.forEach(element => messages.append(element)) } else { messages.insert(0, json) } } } Timer { interval: 5000 triggeredOnStart: true running: settings.user_id !== 0 && ws.status !== WebSocket.Open onTriggered: { ws.active = false ws.active = true } } ColumnLayout { anchors.fill: parent ScrollView { Layout.fillHeight: true Layout.fillWidth: true ListView { width: parent.width model: messages verticalLayoutDirection: ListView.BottomToTop spacing: 5 delegate: RowLayout { width: ListView.view.width spacing: 0 Item { width: 5 } Pane { Layout.maximumWidth: parent.width - 10 Material.elevation: 6 Label { anchors.fill: parent wrapMode: Label.WrapAtWordBoundaryOrAnywhere text: "" + nick + " - " + new Date(created_at).toLocaleString(locale, Locale.ShortFormat) + "" + msg } } } } ScrollBar.horizontal.policy: ScrollBar.AlwaysOff } ToolBar { Layout.fillWidth: true enabled: ws.status === WebSocket.Open RowLayout { anchors.leftMargin: 10 anchors.fill: parent TextField { Layout.fillWidth: true id: messageF onAccepted: postButton.clicked() } ToolButton { id: postButton enabled: messageF.enabled text: ">" onClicked: { messageF.enabled = false var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (xhr.readyState === XMLHttpRequest.DONE) { messageF.enabled = true if (xhr.status === 200) { messageF.clear() } else { console.error("Error posting message: ", xhr.statusText) } } } xhr.open("POST", "http://" + settings.server + " /messages"); xhr.setRequestHeader("Content-Type", "application/json"); xhr.send(JSON.stringify({ user_id: settings.user_id, msg: messageF.text })); } } } } } } Here we have a WebSocket object. It’ll will try to connect to the end point for WebSockets that we created. Once connected, the TextField will be enabled to send messages. From the WebSocket object, we only care about receiving messages. It could also be used to send messages, but we are sending them with plain HTTP so that you can see that the messages are being pushed by the server itself. The TextField is disabled while sending the message, to avoid sending duplicated messages. A demonstration video for this blog is in the works. We’ll add to this blog the links to the video once it’s available, so keep coming back to this page to check. Part two of this blog series will show you how to add Firebase. That’s another goodie that’s in store for you in the coming weeks. About KDAB If you like this article and want to read similar material, consider subscribing via our RSS feed. Subscribe to KDAB TV for similar informative short video content. KDAB provides market leading software consulting and development services and training in Qt, C++ and 3D/OpenGL. Contact us. The post Qt Allstack I – Setup appeared first on KDAB. [Less]
Posted about 2 years ago by Srirupa's Blog
About Me I’m Srirupa Datta, a third year undergraduate studying Electrical Engineering at Jadavpur University, India. This year, I have decided to take part in Season of KDE and will be working on extending the Ellipse Assistant Tool to add support ... [More] for Perspective Ellipse in Krita. My Introduction to Qt and Krita As a painter who also likes software development, I first came across Krita when I was looking for digital painting applications on Windows. Later on, I learned about open source and decided to start contributing as a way to build my development skills while working on something that I myself use. I spent the last few months of 2020 building Krita, first in Windows (a nightmare :P), then in Linux, and started learning Qt since I was already familiar with some basic C++ from school. Since then, I have fixed some minor bugs in my free time, including Warn when exporting layers with multiple transparency masks into .psd and Convert Colorize Mask to Paint Layer before splitting layers. After becoming somewhat familiar with Krita’s huge codebase, I started looking for something more challenging to implement and came across this Feature request: Circle in a Square Assistant Tool . Once I learnt about Season of KDE, I decided to apply for it and cooked up a proposal to implement this feature request and Halla was kind enough to agree to mentor me! Project Details Overview This project aims to create a tool that is essentially an adjustable four corner mesh that always has an ellipse inside it which touches the sides, in order to draw ellipses in perspective with ease. The attachment below, provided by Hector demonstrates how the tool is supposed to work: Benefit Developing this tool would make it easier to draw ellipses in perspective. Currently, to draw a circle in a square with the ellipse tool, one needs to draw the diagonals of the square and correctly align the axis of the ellipse to the square, which is rather complicated since it takes time to set a concentric ellipse with the current ellipse tool. With the circle in a square tool, one just needs to set the four corners of the square. Hector, who created this feature request is also willing to test this feature while it’s being developed! [Less]
Posted about 2 years ago by Falkon
New Falkon version is now out. Changelog Add support for Screen Capture (Qt 5.13.2+) Option for internal PDFium-based viewer (Qt 5.13+) CookieManager: Add support for selecting more than one cookie at once Correct the sorting for cookies white- and ... [More] black- lists Speeddial: decode initialScript data as Utf8 Set current tab as parent for duplicated tab add support for detaching tabs via context menu (BUG 443947, !22 from Fran Rogers) BookmarksManager: Don’t allow to create bookmark without parent Remove the padding of the bookmark bar Added context menu item to bookmarks toolbar for creating folder Preferences: Add links to KDE store for installing themes/extensions OcsSupport: Add initial support for downloading themes/extensions Add filter to preferences extensions page SideBarManager: Clear active sidebar upon close Added focus for searchbar in history sidebar & bookmark sidebar SearchToolBar: Remove warning message that no results were found SearchBar - Show number of matches found on page search added pause/resume for downloads Prompt user when KWallet is disabled KDEFrameworksIntegration: Create Purpose menu in init GreaseMonkey: Fix detecting *.user.js urls Added QLoggingCategory to Falkon Handle web push notifications with Falkon notification system Fix name and set desktop entry for notifications NetworkManager: Fix QtWebEngine warning about registering schemes Don’t use subfolders for QML/Python extensions ThemeManager: Implement removing locally installed themes Plugins: Implement removing locally installed plugins Plugins: Use embedded JSON metadata for shared library plugins RunAction plugin: set alternatingRowColors for listWidget VerticalTabs: Accept click event on tree collapse VerticalTabs: Draw unloaded tabs with “Disabled” color palette VerticalTabs: Add child tab with middle click on “New Tab” button VerticalTabs: Corectly recognize tree collapse / expand button VerticalTabs: Also override Ctrl+PgUp/PgDown shortcuts Download: falkon-3.2.0.tar.xz (sig signed with EBC3FC294452C6D8) [Less]
Posted about 2 years ago by Adriaan de Groot ([ade])
It’s been a week of obscure-bug-hunting. A bug report (PR in FreeBSD words) pointed out that KOrganizer was crashing. Since it works on Linux, it falls to the KDE-FreeBSD team to figure it out. A quick session with lldb shows it’s a ... [More] nullptr-dereference. Since we had exciting nullptr differences before, my first idea is to look for smart pointers. And they’re there, and can lead the unaware into a bog, a fen, a morass of undefined behavior (but it happens to work on Linux). Symptoms and Implementations A “smart” pointer manages a chunk of memory it points to. Examples in Qt include QScopedPointer and QSharedPointer. C++ standard examples are std::shared_ptr and std::unique_ptr. Smart pointers need a particular implementation: the Qt source code implements the Q-flavored ones (in Qt source code, and there’s only one Qt source code), but the standard ones are implemented by multiple standard libraries. There’s GNU libstdc++ and LLVM libcxx, for instance. There are differences in the implementations. One important difference lies in the implementation of the destructor of a std::unique_ptr. The LLVM implementation replaces an internal pointer by a nullptr and then calls the destructor of the held object, while GNU calls the destructor of the held object and leaves the internal pointer alone (the std::unique_ptr is being destroyed anyway, so why bother updating the pointer-value). This becomes visible in some situations where a not-completely-destroyed smart pointer is used: with the GNU implementation it may still hold a valid pointer, with LLVM it holds nullptr. Some will crash, some will not – it doesn’t really matter because to get into this situation you need to be in Undefined Behavior territory anyway and you should be glad that your computer doesn’t catch fire, fall over, and then sink into the swamp. The visible symptom in a backtrace is an unexpectedly nullptr “smart” pointer. D-ptring A d-ptr is a technique for maintaining binary-compatibility when a class’ implementation details change. It’s explained fairly well on the Qt wiki. When the size of (instances of) a class changes, all of its consumers need to be recompiled. Consider this very simple class that describes the state of a Game. It can print the state of the game, too. struct Game { int score = 0; void print() const { std::cout << "Score=" << score << '\n'; } }; If we now realise we need to keep track of which turn it is in the game, we add a data member – but that changes the size of the instances of the class to accomodate the new data member. Now all the code that relies on class Game needs to be recompiled. struct Game { int score = 0; int turn = 0; void print() const { std::cout << "Score=" << score << '\n'; } }; To avoid having to recompile consumers, the d-ptr technique moves the implementation details – those pesky data members – to a private class that consumers don’t know about. The Game class now holds a single smart pointer to that private class. Pointers are always the same size, so the Game class won’t change size any more no matter what we do with the implementation. In Qt-related code, it is common to have a pointer from the internal, private class instance, to the owning instance. It’s just common practice, and this is called the q-ptr (a “q” is just a “d” pointing in a different direction, get it?). So we introduce a private inner class, and give the Game class a smart pointer to an instance of that. While we’re at it, give the game a method to win some points. This is the last time we need to recompile our consumers. This example code puts everything in one declaration of class Game and fits in one file. Typically you would hide the implementation detail in a separate translation unit or a .cpp file. So “last time” is a bit of a lie: in a typical realistic implementation this would be the last time. class Game { struct Private { Game* const q; int score = 0; int turn = 0; Private(Game* owner) : q(owner) {} }; std::unique_ptr const d = std::make_unique(this); public: void print() const { std::cout << "Score=" << d->score << '\n'; } void win(int n) { d->score += n; } }; Diagram with d- and q-pointers between classes. The d-pointer is smart, unlike q. Now we can add data members all we like in the Private class, and it won’t affect the size of Game objects and everything is hunky-dory – except that the stage has been set for the bog monster. Object Lifetime In C++, objects have a lifetime. Before an object is alive, it cannot be used. As it lies dying, special rules apply, and once it is dead, it cannot be used. Accessing an object outside of its lifetime (there isn’t an object then!) is Undefined Behavior, and I hear the bog monster has quite shocking teeth. Here’s a pretty innocuous idea: when the game ends (e.g. it is destroyed), we should print the score as well. There’s already a function implementing this for us, and in the interest of not adding functions to the Game class, let’s use the destructor of the internal Private class to do the work: we’ll just add one line, ~Private() { q->print(); } See, when the Game destructor is called, the Private destructor is called, and we just print the score. (Narrator: this was not the case) A cute little bog monster Depending on the environment your bog monster lives in, it may be a cute one or a monstrous one. Keep in mind that q is a raw pointer, and it points to the owning Game instance, which is “in the process of being destroyed” (not true, more on that later). We call the Game::print() method, which dereferences the std::unique_ptr to get at the score variable held by the Private instance. The GNU implementation still has a pointer to memory, score is read, and the print() function seems to work. The LLVM implementation has already set the pointer to null, so the dereference falls over with a SEGV. It doesn’t really matter, since it’s undefined behavior to call print() at all here. Lifetime Details A fire-breathing bog monster Thinking about the destruction of Game and all of its data members (including Private, which is held through that std::unique_ptr) as “one action” is conceptually convenient until things go wrong. Then the details of object lifetime matter: The destructor ~Game is called (the destructor starts). At this point the lifetime of the object is over. Dereferencing pointers to this object is undefined behavior from now on. That includes the q pointer held by the Private object. Basically, q is a dangling raw pointer. During the destructor call it is still permitted to call member functions from the destructor (with some caveats), so calling print() here would be ok. Destructors for members are called after the destructor of Game has run – so Game is really really dead by now. Only now does the destructor for the member d run, so that’s how we get to the destructor of Private. There is no Game object any more, so using q is undefined behavior (still). We can apply the same analysis to the destruction of Private even without looking at Game. Suppose a Private object is owned by some specific std::unique_ptr d, and we call d.reset() to destroy the held object. d.reset() might destroy the held object first, and then change the internal pointer to null, or might do it the other way around, it doesn’t really matter. The destructor ~Private is called. At this point the lifetime of the object is over. Dereferencing pointers to this particular object is undefined behavior from now on. That includes the internal pointer of d. During the destructor call it is still permitted to call member functions (and methods of other objects that are still alive and free functions and everything). So q->print() is legal to call, but .. Dereferencing d in the implementation of print() is either going to dereference an invalid pointer to the Private object whose lifetime has ended, or dereference an invalid null pointer. Either way is Undefined Behavior. Takeaway Looking for a pithy rhyming takeaway I can’t get more than If you Q_Q it, don’t do it. Which is overly dismissive of Q_Q. Somewhat less pithy: When d-pointering, the destructor of the Private class must not use the q pointer. Following that advice avoids both of the scenarios leading to UB sketched in the previous section. It’s easy to overlook, and seems innocuous, until it crawls out of the bog and bites someone (in Kldap, this has been patched). [Less]
Posted about 2 years ago by Elvis Angelaccio (elvisangelaccio)
Next week I’ll be starting a new job after 4 years since my last one. I’m very happy because I’m finally going to work with C++ and Qt; it was not easy to find this kind of job on the Italian market… I will also move from Rome to Florence which ... [More] should be a nice quality of life upgrade. My new employer is Develer. Some of you might know it because they organize the annual QtDay. They also sponsor other conferences such as the italian Pycon and they are in general friendly to open source. In the past few months I had very little time for KDE stuff, hopefully this will change once I settle down in the next few weeks. [Less]
Posted about 2 years ago by Kate
Bugs, bugs, bugs… The Kate & KTextEditor projects have at the moment 189 open bugs. That is a lot given only a few people work on this projects regularly. Therefore it would be really appreciated if people could help out with fixing. If you have ... [More] spare time and you are interested in help to improve our projects, head over to our long list of bugs. Some might even be already fixed and just need to be tried out with a recent version. Other might lack ways to reproduce, providing such ways or even just asking the reporter and setting the bug to the NEEDSINFO/WAITINGFORINFO state will help us to keep some better overview. Contributing to Kate & KTextEditor is easier then ever nowadays. Head over to our build documentation, with kdesrc-build it is very easy to setup some development environment on any recent Linux/BSD distribution. We have already a really large number of merge requests that can be used as hints how to actually submit you patches and how the process works. Normally, if you are responsive to our feedback, I would say the patch contribution works really well since we are on our GitLab instance. Wishes? Beside actual bugs, we have 128 wish list items lingering around. If you need some ideas what could be implemented, you can take a look at that list, too. We have some more high-level collection of ideas to work on our GitLab instance. Naturally, you might be more interested in contributing patches for your own ideas, that is definitively welcome! These lists are just collection of potential things you could work on if you have no own ideas but want to help out with feature development for people that can’t implement them on their own. Rambling of a maintainer about bug reporting Now, after reaching out for a bit help with the bugs and wishes we already have around, some feedback for the actual bug reporters. I naturally appreciate that you report bugs and wishes. Only with such feedback we are able to know what is broken and what is wanted. This is very important. Sometimes even I myself just report a bug for some crash or other misbehavior that I can’t directly fix or reproduce, just to have it marked for future review. And it is totally valid to report wishes you have for our stuff. But, there is the other side, too, our team. We work on this in our spare time, no Kate or KTextEditor main contributor was ever paid for that. I guess more or less all current work is done purely as hobby and out of fun or responsibility for the code base. Implementing new features is fun, normally you add stuff you wants to use yourself, therefore the work on that is gratifying. On the other side, fixing bugs is often tedious work. Even more, if the bug doesn’t hurt you at all. Just to pick a random example (we had a input method bug like this, but without any of the negative feedback mentioned at the end, that’s fiction for that bug): Some input method breaks. I don’t use any complex input method, therefore I am not hurt by that at all. As long as I can type äüöß… I am happy. But yes, naturally, such a breakage is very bad for a large part of the user base. I personally have a hard time to fix such stuff myself, as I have close to zero experience with it and have not even implemented the current support for that. In such cases if I find the time I try to reach out for people that are more fluent with that and eventually such things then get worked out. If nobody is found that actually cares for such things or no such person has time, such bugs will stay. No amount of pinging, shouting or similar inside the bug will help. I feel responsible as maintainer, but if I have not the needed skill or time or both nothing will happen. I can not coerce somebody to fix it even if I know somebody that has the needed skills. Nor do I want to. Over the last years, we had such negative events in not that few bugs. Sometimes it seems like people think we are actually their paid support. No, we are not. If you report some issue and we ignore it, ping us friendly, that can’t hurt. For sure at least I did already just ignore bugs because I didn’t read them well enough or just skipped them completely. Some gentle ping is ok, but some “this is very important and must be directly fixed or else I will never ever use this again” won’t help you. The same if you want some behavior change or feature and we don’t want to implement this. Let’s say somebody wishes in a bug “I want the option that the text is painted in rainbow colors with some configurable rainbow color scheme”. And then I and others in the team decide this is a feature out of scope for us and we won’t do it. You should respect this. Even more if you actually don’t want to contribute such a behavior change or feature yourself. Executive summary about bug reporting Bugs & wishes are very welcome, you help us to improve our projects by reporting them. But keep in mind, we have not sold you some product with a support contract that will earn us our living. We provide you with hopefully valuable tools in our spare time and we would appreciate at least a bit respect for that in the communication with us. Comments? A matching thread for this can be found here on r/KDE. [Less]
Posted about 2 years ago by Programmer's Hideaway
Background While working on Rust bindings for KConfig as a part of Season of KDE 2022, I came across a few problems while trying to represent QFlags in Rust: Most QFlags are defined as C++ enums in which multiple members can have the same value. ... [More] This is not possible in Rust enum. It is possible to enable multiple flags using BitwiseOr. Rust enums cannot do bitwise operations. This post will guide you through the various implementations I came up with and their tradeoffs. The C++ enum The enum I was trying to implement was KConfig::OpenFlags. The enum is given below: enum OpenFlag { IncludeGlobals = 0x01, ///< Blend kdeglobals into the config object. CascadeConfig = 0x02, ///< Cascade to system-wide config files. SimpleConfig = 0x00, ///< Just a single config file. NoCascade = IncludeGlobals, ///< Include user's globals, but omit system settings. NoGlobals = CascadeConfig, ///< Cascade to system settings, but omit user's globals. FullConfig = IncludeGlobals | CascadeConfig, ///< Fully-fledged config, including globals and cascading to system settings }; Implementation 1: Using Rust modules This method uses a combination of Rust modules and consants. The sample implementation is as follow: pub mod OpenFlags { type E = u32; const INCLUDE_GLOBALS: Self::E = 0x01; const CASCADE_CONFIG: Self::E = 0x02; const SIMPLE_CONFIG: Self::E = 0x00; const NO_CASCASE: Self::E = Self::INCLUDE_GLOBALS; const NO_GLOBALS: Self::E = Self::CASCADE_CONFIG; const FULL_CONFIG: Self::E = Self::INCLUDE_GLOBALS | Self::CASCADE_CONFIG; } fn something(flag: OpenFlags::E) {} Advantages Const is replaced at compile time, so no performance cost. All values can be documented in the same way using Rust comments. Multiple flags can be activated. Drawbacks Not an enum. Just a collection of constants. Implementation 2: Using const in Impl This method defines the problematic members as const in impl. The sample implementation is as follows: #[repr(C)] pub enum OpenFlags { IncludeGlobals = 0x01, CascadeConfig = 0x02, SimpleConfig = 0x00, FullConfig = 0x01 | 0x02, } #[allow(non_upper_case_globals)] impl OpenFlags { const NoCascade: Self = Self::IncludeGlobals; const NoGlobals: Self = Self::CascadeConfig; } fn something(flag: OpenFlags) {} Advantages Enum, for the most part. Drawbacks Inconsistent documentation. The constants don’t show up as enum variants. Multiple flags cannot be activated Implementation 3: Converting standard Rust enums when passing to C++ This method uses standard rust enums. The sample implementation is as follows: pub enum OpenFlags { IncludeGlobals, CascadeConfig, SimpleConfig, NoCascade, NoGlobals, FullConfig } impl OpenFlags { type E = u32; const INCLUDE_GLOBALS: Self::E = 0x01; const CASCADE_CONFIG: Self::E = 0x02; const SIMPLE_CONFIG: Self::E = 0x00; pub fn to_cpp(&self) -> Self::E { match self { Self::IncludeGlobals => Self::INCLUDE_GLOBALS, Self::CascadeConfig => Self::CASCADE_CONFIG, Self::SimpleConfig => Self::SIMPLE_CONFIG, Self::NoCascade => Self::INCLUDE_GLOBALS, Self::NoGlobals => Self::CASCADE_CONFIG, Self::FullConfig => Self::INCLUDE_GLOBALS | Self::CASCADE_CONFIG, } } } fn something(flag: OpenFlags) { let flag = flag.to_cpp(); ... } Advantages Completely Enum. Documentation works as expected. Drawbacks Function call every time passing from Rust to C++. I don’t think this will have much performance penalty, but still worth mentioning. Cannot set multiple flags at once. Eg OpenFlag::IncludeGlobal | OpenFlag::CascadeConfig not possible Implementation 4: use bitflags crate This is the implementation that I finally settled on. The implementation is as follows: use bitflags::bitflags bitflags! { /// Determines how the system-wide and user's global settings will affect the reading of the configuration. /// This is a bitfag. Thus it is possible to pass options like `OpenFlags::INCLUDE_GLOBALS | /// OpenFlags::CASCADE_CONFIG` #[repr(C)] pub struct OpenFlags: u32 { /// Blend kdeglobals into the config object. const INCLUDE_GLOBALS = 0x01; /// Cascade to system-wide config files. const CASCADE_CONFIG = 0x02; /// Just a single config file. const SIMPLE_CONFIG = 0x00; /// Include user's globals, but omit system settings. const NO_CASCADE = Self::INCLUDE_GLOBALS.bits; /// Cascade to system settings, but omit user's globals. const NO_GLOBALS = Self::CASCADE_CONFIG.bits; /// Fully-fledged config, including globals and cascading to system settings. const FULL_CONFIG = Self::INCLUDE_GLOBALS.bits | Self::CASCADE_CONFIG.bits; } } fn something(flag: OpenFlags) {} Advantages Multiple flags can be used together. Documentation is consistent. Drawbacks Not enum. Shows up as struct in docs. Documentation Screenshot Conclusion I think I will be using bitflags for representing all QFlags in kconfig for the foreseeable future. [Less]
Posted about 2 years ago by Volker Krause
Since the last summary two month ago KDE Itinerary has had its 21.12 release and is already receiving new features for the next one, such as manually added train and bus trips or new additional information displayed in the timeline. New Features ... [More] Manually added train trips Getting train or bus trips into KDE Itinerary so far was only possible by importing the corresponding ticket, manual editing wasn’t available. This makes sense as in order to function properly we need trip data that actually matches the schedule and contains coordinates or identifiers for efficiently querying for realtime updates. However that’s a problem for users with some form of flat rate ticket, they might not have a corresponding ticket for each trip. Therefore it will become possible to add train or bus trips from a online journey search, basically as if we’d have an embedded KTrip for this. To make this work conveniently we needed a location picker, which involves a name-based online search for stations. KPublicTransport provides that since the very beginning, but is does have a problem: which online backend do we query? Without further instructions that will be all 70+ of them, which is very inefficient and will produce poor results. KTrip addresses this by asking the user to explicitly select an online backend. That works, but given the amount of backends we have this isn’t really ideal. For Itinerary we took a slightly different approach. For the location search you just have to select the corresponding country, and with that information KPublicTransport is now able to automatically pick as few as possible backends to perform the location search. Train station picker in KDE Itinerary. Since Itinerary knows the location you will be in at a given time, that is already selected for you correctly in most cases, and a shared location history further speeds up the location search. Unfortunately there is still one blocking issue, this needs the so far unreleased kirigami-addons module for selecting the departure time. We therefore cannot enable this feature unconditionally yet. If your package/platform has kirigami-addons and you can’t wait to try this, it is accessible in the timeline context menu with development mode enabled. Public holidays in the timeline KDE Itinerary can now also show more information in timeline day header elements. Most prominently this is used to show public holidays, at the location you are in on the given day. KDE Itinerary timeline day header showing a public holiday. This should help to avoid surprises like the public holiday in the middle of the week during Akademy 2018 :) Currency conversion rates Similarly, the timeline location information cards now also contain currency information when entering a country with a different currency than is used in your home country. When enabled this also includes the current conversion rate (off by default, as this requires online access). Currency information in the itinerary timeline. Infrastructure Work On Android there is a standardized mechanism to bring up the user’s favorite map application for a given location, using geo: URIs. Those work on Linux too, but so far we couldn’t rely on them, requiring hardcoded alternatives which result in duplicate code and sub-optimal user experiences. This has two reasons: No graphical user interface for selecting the preferred application to handle geo: URIs, in case there are multiple applications able to handle them. No guarantee that there would be anything able to handle geo: URIs at all. Both problems have now been addressed. With Plasma 5.24 there the “Default Applications” system settings module will allow to pick the default map application. Plasma System Settings default geo: URL handler configuration. And with KDE Frameworks 5.91 there is now always at least a fallback handler for geo: URIs that translates them to corresponding OpenStreetMap URLs and opens those in a web browser. This is not limited to OSM, it only takes a .desktop file to add support for other web-based maps (see MR for examples). While this will help Itinerary it’s in no way limited to that, similar code is found in KAddressBook or Kalendar as well for example. Fixes & Improvements Travel document extractor Only consider rectangular strokes for the PDF vector barcode detection. This fixes other vector graphics in e.g. Dutch vaccination certificate PDF documents interfering with vector barcode detection. Make the VIA Rail extractor work both with a full PDF or just the barcode. Add new extractor scripts for Amtrak and Skymark booking emails. Fixes for the SNCF and Vitolus extractor scripts. Public transport data Make platform comparison consider platform section information. This fixes erroneous platform change warnings when schedule and live data differ in the amount of platform section details only. Fix Navitia location queries when querying for multiple different location types. Support EFA floor level difference attributes. This results in more reliable indoor navigation instructions. Support the alternative Hafas platform format. Indoor maps Fix station map platforms being selected wrongly without live data. Improved translations of OSM map element details. Improved opening hours parsing for expressions containing common errors or other deviations from the specification, or use localized wording. Itinerary app Fix issues with wrong trip groupings due to the same city detection getting confused in densely populated areas. Hide the booking section for train and bus reservations when empty. That can happen now that we can create such trips manually. Fix health certificate ordering when facing invalid dates, and remember the last selected one. Improve the display of vaccination times. Contribute Feedback and travel document samples are very much welcome, but with travel remaining difficult there are plenty of other things that can be done from home as well. The KDE Itinerary workboard or the more specialized indoor map workboard show what’s on the todo list, and are a good place for collecting new ideas. For questions and suggestions, please feel free to join us on the KDE PIM mailing list or in the #kontact channel on Matrix. [Less]