Re-designed User Interface #366
Replies: 52 comments 16 replies
|
Generally seems pretty solid. Streamlines whats already there. Do the tune buttons and such immediately turn red, or turn yellow (Or whatever) when they're armed, then red when active? |
|
While it's fairly easy to drop in an "armed" but not active state for a button it gets fairly expensive from UI churn standpoint. Very similar to how the old Mode Button worked just to update its text display on every UI tick. When the button is toggled on/off it's a simple, single boolean. When you start running state checks like I did for the buttons that need to mimic the context menu, so they have three states, that increases the churn on the main thread. If I was designing an interface for Mac-only using SwiftUI, not a big deal. But this is Qt. |
|
Right now, the HB button is redundant too. Updating the text on that to show HB, or HB + ACK, or a timer countdown, is quite expensive from a churn standpoint on the main thread, updating that stupid button once every UI tick. It's just as bad as the Mode Button was, just not as complicated. But it's still bad UI design. |
|
Program launched 2026‑09‑12 20:37:14 -> 2026‑09‑13 08:37:55 ~12 hours of continuous uptime. Main thread almost entirely parked in the Cocoa/Qt event loop waiting on mach_msg - mostly idle, not stuck or spinning. The only real work is occasional waterfall redraws (CPlotter::drawData) and rig-status polling via HamlibTransceiver::poll), both light and intermittent. Thread_1021482, running JS8::Worker::run: this is the only thread doing real work. ~70% are inside FFT decoding (fftwf_execute, apply_r2hc) for JS8 "Mode E" (JS8 Slow mode). The remaining samples are blocked on a semaphore waiting for the next audio buffer. I'm pretty happy with the GUI (Main) thread. Considerably more efficient than it was with the old design where it was continuously churning on every UI tick, mainly to refresh that Mode Button. But also that status label that was on the status bar constantly flashing Decoding/Receiving. That label not only didn't convey anything useful, a UI element like that is quite expensive from a UI refresh standpoint in the amount of churn it requires on the Main thread to constantly update the text on that label with a Qt-based application. The poll()/qt_safe_poll calls now aren't busy polling, they're blocking waits. This puts the thread to sleep in the kernel until a file descriptor becomes ready or a Qt timer fires. No more spinning on the UI refresh loop looking for update events. The diff is attached, fairly extensive change. I did the initial layout change with Qt Designer, which is not exceedingly user-friendly to work with. Designer tends to want to re-write portions of the XML code that you don't want touched because you want to do it programmatically in C++. I spent almost a month fixing XML code that Designer messed up (in between going nuts trying to read XML tags and having to take a break from it). Hooking up the UI_Constructor was easy. There was several times I was tempted to send Qt down the road and just code it in Swift. But in the end it was 4,622 lines of code changed, with a reduction in the total codebase, over just about 2 months of working on it in my spare time. |
|
If you have a Mac build ready to go, I'll give it a try. And yes, Qt Designer is much like hostage negotiations. For all of my new stuff, I just set up the widgets in code directly. Ends up being at least as fast to implement, much easier to understand in the long run (it's all right there in front of you instead of hidden in some non-revealed setting) and way less brittle. |
|
The changes Chris described make sense, and they look like they could help the program run smoother and faster. |
|
@Joe-K0OG Hi Joe, right now it is what I consider to be a running prototype after I got the backend hooked up the front end, plus got all the hidden background features working again. The UI has only been tweaked for Mac, since I don't develop on Windows or Linux. It's not the size of the screen that matters, it's the resolution that it runs. As it is right now, it will run and display beautifully on a iPad A16 with 11" screen hooked up to the Mac with SideCar. Minimum width of the program window is 1000px with no button overlap, and a suitable border (or "buffer") between buttons. For instance, in your Windows screenshot, that hidden label that says T-100D-Dar should not even be showing. That label has a self-destruct timer of 5000ms on it and none of that has been tweaked for Windows or Linux yet. It will currently display on Mac in the control bar by moving the buttons to the right. But it's not going to stay there because that's a bad design. The code that fires it is the showStatusMessage() member function of the UI_Constructor class, it's on line 599 in mainwindow.cpp. So what I have at present is a proof-of concept that a UI redesign, done properly, can produce a more computationally-efficient "engine" for the UI. And it does that in a big way by getting rid of tons of inefficient code bloat that was bolted on as an afterthought a bit at a time when the original UI was designed. I also reviewed the waterfall drawing code, and that's fairly impressive compared to the rest of the UI. I believe @bazineta rebuilt that when he did the Fortran->C++ refactor, and it shows. Compared to the WSJT-x code our waterfall drawing code is a sports car vs what WSJT-x has, which has the fuel efficiency of a city bus with an old Detroit 8V-71 in it. |
Allan wins the Analogy Of The Day! That's a good one! |
|
@bazineta obviously I do have Mac build but nothing that I've sent into Apple yet for notarization since I consider it to still be a prototype. If you want to try it out you can run git apply on the diff that I posted and it will generate the code if you run that on current master. I'm currently building with Qt 6.12.0-beta4, patiently waiting for the Qt Group to release 6.12.0 stable on Sept 23. So you'll have to build that library from source and make sure to include QtWebSockets in the modules when you build it. DO NOT use beta4 installed with the Online Installer. You need a build optimized for the M-series processors and GPU, and you must build FFmpeg first and package that with your Qt build with -ffmpeg-deploy. Plus use the -no-opengl flag as well so your Qt build uses the Apple Metal API instead of OpenGL. If you build on MacOS 26, without that it will link OpenGL to the binary. Recommended is to build your Qt library on MacOS 27 beta8 with clang-21, which no longer has OpenGL in its SDK. |
|
I generally like the direction of this redesign. Reclaiming some vertical space makes sense, and separating the UI from all the constant per-tick updates of the old Mode button sounds like a worthwhile cleanup. A few UI details stood out to me, personally, though, mostly regarding the distinction between controls and status information: The frame/timing indicator is purely status information. I actually liked it at the very bottom in the old layout, but perhaps an even cleaner solution would be to integrate it more subtly with the clock at the top. Both are time-related, and the frame counter probably does not need as much visual weight as it currently has. A smaller display next to or underneath the clock might work well. I am also a little unsure about the frequency indication at the bottom. It looks somewhat like a button in the screenshots — is it actually interactive, or is it just a display? The base frequency and offset are already visible at the top. The resulting actual frequency is useful information, but I wonder whether it is important enough to permanently occupy another prominent UI element when the same information can already be derived from the two values above. The same question applies to the WPM/CPM display. I assume this is primarily status information determined by the selected mode. If so, could it perhaps be integrated into the Mode button itself instead of using a separate element? For example, the mode could remain the primary label, with WPM/CPM shown as smaller secondary information or tooltip. Personally, when typing, I look at the calculated number of frames / expected sending duration more than the WPM/CPM display. If screen space is limited, I would probably give frequency, mode and TX/RX state higher priority than permanently displaying the exact WPM/CPM figures. More generally, I think it would be useful to keep a clear visual distinction between controls and status indicators in the new bar. Mixing both into similarly styled elements saves space, but makes it harder to tell at a glance what is actionable. If something is clickable, it should clearly look clickable; if it is passive information, it should probably have less visual weight. I also agree with Joe's point about the small-window layout. The double-row approach looks good with enough width, but I would be hesitant to make ~1000 px a practical design target that the UI depends on. JS8Call is also useful on smaller laptops, tablets and remote/VNC displays, so having the controls gracefully compress, wrap, or move less frequently used items into an overflow menu would be nice. And I agree that the JS8 Weak Signal Messaging text in the middle probably does not need permanent screen space. I would rather leave that area available as flexible space (or for status information?) than fill it just because it is there. Its just a few thought to be discussed, nothing more. |
|
@Joe-K0OG My primary focus right now has been supporting MacOS 27 when it comes out in a little over a week. It requires Qt 6.12 minimum, it's no coincidence that they are being released on the same day. But I have not moved the libraries on js8lib, and won't move them until 6.12.0 reaches stable status. It is Qt's next LTS release for commercial developers so it is the obvious target. I figured I'll deal with Windows and Linux when that happens and if there's deprecations in the code that have to be fixed, I'll fix them. |
|
@Beiri22 all good observations. As I get this closer to being something that we might want to consider, the obvious direction would be an open PR where I got the baseline working and correctly organized, but the community can join in working on it and submit their own changes that actually makes it practical. Right now it's not practical yet. |
Allan has a good point here. What do we need some of these UI elements for? The frequency label is redundant. That is clearly shown on the dial freq display + whatever you have the cursor set to on the waterfall. If the user chooses to not display the waterfall, then you have a backup in the header bar right under the frequency display. That's gonna go. Same applies to the progress bar. What good does it do? The periods are clearly shown on the waterfall. I think that's a holdover from WSJT-x just because it had one. That's gonna go. The wpm label? Who really cares about that? Whatever your mode speed is determines ave wpm. I think Allan is right - who actually looks at it? It's one of those reactor control room things that gets tacked on "just because we can" to make the UI busy. But whether it says 10 wpm or 60 wpm is not really useful and it doesn't blink red if the transmitter core is going into meltdown from running at constant 60wpm. That's gonna go. I have to look at that hidden status message label too to see what kind of useful information that thing actually spits out when it pops up for 5 seconds, then goes away. That's probably another UI element that's there "just because we can". But does it actually convey any useful information, or is it just UI "clutter"?
This kind of thing we want to stay away from. While the jack-of-all-trades-master-of-none button design might seem cool, it adds a considerable amount of code bloat and UI churn to make buttons do this with Qt because you are constantly updating that button display on every UI tick. If it's just an on/off button, the thread can sleep until an event handler fires to turn it on or off, and then it just switches a boolean and runs different code logic for on state or off state. We still have this problem with timer displays on the HB and CQ buttons, as well as the sending time on the SEND button. I remember one discussion where a developer who has contributed code here said the HB button needs an extra context menu that allows setting it to just click on it and sent a HB instead of having to right click and select Send Now. This was from one of the big proponents of HB timers being the best thing since sliced bread so he's got a timer going on there CONSTANTLY spitting out HB's every 15 minutes (and is probably blocked now with the new HB ACK suppression feature). Joe pointed out, just select "On demand / do not repeat" and it already does that. When even developers don't understand how those jack-of-all-trades-master-of-none buttons work, it's not a good design. |
|
There's not going to be any option for legacy layout. Unless somebody wants to code up such a nightmare and put another button or switch in the code for it. For legacy layout there's version 3.0.3. Since the Call Activity is now in its own frame I have the option of making it a dockable panel like the Message Inbox is. I haven't decided to go there yet. |
|
There is 14 call sites in the code for the SEND button in that macro bar, plus a function just to update the text on it, which has six call sites. The macro bar is designed the same way those buttons in the header bar were designed. After I get the maze unraveled I'm not even sure we need this button at all
|
|
I haven't committed any changes for this latest one yet as I haven't decided how to handle it. The code for the macro bar is scattered all thru mainwindow.cpp and integrated with the backend "engine" as a monolithic design. We have buttons like the HB and CQ buttons with timers integrated into them updating button text on the on-the-second UI tick. And decisions have to be made, like do we actually still need that macro bar Halt button when clicking on the TX on/off button can now do the same thing by stopping a transmission and set it to its previous state? So I haven't decided if I should first consolidate all this code into a createMacroBar() function like I did with the createControlBar() function where it's easier to work worth in one place. Or fix the event handler first, that has calls scattered all thru the code. Those macro buttons should not have to be updated on the on-the-second UI tick as this is another expensive operation from a UI churn standpoint with Qt. The HB button changes its text from "HB" to "HB + ACK", plus displays a per-second counter, all running thru that UI tick. It should be just a static QPushButton with a right click context menu. We already have an indicator with a green HB ACK button that shows if HB ACK is on or not. Duplicating that on the HB button itself is redundant. But what to do with the timer text display on it? I could move that to the header bar with a timer display in the spacer area between the frequency display and the callsign/clock for both HB and CQ timers. This is the way Fldigi does it with a timer-controlled transmission - it doesn't display it on the button, and instead the countdown timer is in a little widget in the header bar. And I had other thoughts;
Thoughts? |
|
So everything is wired up and working with the present design. Put it thru the wringer and happy with it. The old QCache method used in processCommandActivity() has been moved to a SQLite3 database method. There was a 15 minute cooldown timer on ALLCALL related commands (including HB) and the data for this was stored in memory with QCache. So I removed the old UI setting to suppress HB ACK at 55 minutes (which subsequently blocked that callsign) and moved this cooldown to a 55 minute interval in a SQLite3 database that won't block the callsign. So if you have somebody that fires off HB's like a M122 tripod-mounted M60, your station will not autoreply to that station again for 55 minutes. For any operator who's paying attention and not letting a computer do everything, this does not remove the ability to return enemy fire manually. It just means you have to switch to the Barrett Lite 50 and more carefully pick your shots while your M60 cools down from firing back at that station. And many other things, including some code cleanup, Qt deprecation fixes, integer conversion fixes, etc.. When I found something I didn't like, I fixed it. But I think I'm gonna stop here for now because any sort of PR keeps getting bigger all the time, and makes it virtually impossible to rebase any other PR's on something this big. So I'll probably PR it so it can get its place in the lineup, then put out a developer snapshot release build so people can try it out. This is what we got so far: |
|
You can use the portable stuff in Qt to do the same thing; I'll see about putting together an example extracted from my SDR code. |
|
I used QRhi to connect to the low level Metal API graphics code. In theory, QRhi is supposed to be cross-platform but I got too much Metal-specific code in here and it needs heavy optimization yet although the total GPU time is quite low
Although it did reduce the total cpu load quite a bit. Running on an E-core on a M4 Pro processor that MacOS has throttled down to 433MHz it's only pulling 7.7% on that E-core.
|
|
At the time that'd I'd finished the C++ development, much of the CPU time on my machine was going to drawing the waterfall, which is why there was so much effort there to make it more efficient, e.g., the |
|
Oh, definitely. The goal on the first pass was to get the GPU backend hooked up and get it to run. The GPU is quite finicky on how it handles memory. The Apple M-series fundamentally breaks the rules of traditional graphics cards. Instead of using VRAM it uses UMA. While Apple's design enables massive advantages like allowing a GPU to access up to 128GB of RAM without crossing a PCIe lane bottleneck, it introduces some distinct, quirky coding hurdles. Traditional desktop GPUs are "immediate mode" renderers. Apple’s GPU uses TBDR. Traditional graphics compilers look at a task, guess the maximum amount of memory it could possibly need, and reserve that entire block ahead of time. Leaving a lot of memory sitting empty and wasted. Apple M3 and later allocates memory in hardware, in real-time, matching the exact amount required for the immediate process. This requires super precise memory allocation flags or you'll get a crash. In a standard QWidget, resources are ready in the constructor. In a QRhiWidget, you cannot touch or initialize any graphics buffers until initialize() runs and hands you a valid rendering target. ASAN is your friend when developing code for a M3 or later GPU. So it needs heavy optimization yet. I'm still using the cpu to write to QImage for the handoff to the GPU. But it was a good method to use to get an initial port working. I'll work on that later. |
|
In case it's helpful, here's the approach I've taken with my stuff, and things that I tried and backed away from. Current:
That all works pretty well; arrived at the native window and createWindowContainer() via performance measurements, and that approach won hands down. Downside that I've not addressed at the moment is that the waterfall is display history, not a lossless recording of every FFT; changing its texture dimensions also clears the history. Fixed this in the CPU rendering in JS8Call, just haven't had the time to do the same with this implementation -- most of the time has gone to the P25 decoding, as that's a deep, deep topic. Things I tested and backed away from:
|
|
The circular buffer via texture-coordinate wraparound is precisely what texquad.frag's uv.y = fract(uv.y + rowOffset) already does in my code. Sample with a wrapping offset rather than physically scrolling image data. It is the same underlying idea. Never move the pixels, move the read position. I just selected the standard, battle-tested approach. I do like your R32F + shader-side colorization instead of precomputed RGBA8. Right now my replot()/drawData() compute m_colors[m_scaler1D(v[x])] on the CPU and bake the final color into each uint32_t before upload. Same bytes-per-pixel (4 bytes either way), so no bandwidth win. But it decouples the stored history from the current palette/dB-range settings; changing gain, palette, or dB scaling becomes a shader-uniform tweak instead of requiring the ring buffer to be rebuilt. Right now I got a memory leak in this code that I'm attempting to nail down. I suspect it's in drawLine() or drawData() but I haven't positively identified it yet. The allocation count and byte total in the heap zone keep climbing after 24hrs uptime, outside of normal paging. The decoder pushes new spectrum/waterfall data roughly 4x/second, and I got something that allocates several objects per drawData()/replot() call and never releases them. Tracking down a memory leak is more difficult since on Apple silicon there's no discrete VRAM. GPU allocations, the IOSurface/IOAccelerator (graphics)/owned unmapped (graphics), are drawn from the same physical memory pool as everything else and show up in the process footprint. JS8Call stock code, using a raster backing store, never allocates any of that. Its GPU cost is just normal window-server compositing of a bitmap, not textures it owns. So tracing a memory allocation issue is x2 on Apple silicon, determining if it's in heap code or GPU code. vmmap gives me this, but I have to run it for 24hrs and compare IOSurface, IOAccelerator, and the various MALLOC_* zone totals over time to determine which one it is. If it's IOSurface/IOAccelerator that's growing, that's GPU/window-compositing memory. If it's a MALLOC_* zone growing, that's ordinary heap. |
|
After a couple of weeks of chasing the same memory growth, running the relevant instruments, etc., I think it's an artifact of how Apple charges the GPU memory, and it seems a bit lazy about disclaiming it. About ready to wave my hands and say "it's a Metal thing". |
|
I don't think it's a Metal thing. It could be a Qt thing. But otherwise there's plenty of apps that don't grow memory usage over time. But none (that I know of) that are built on Qt. |
|
@wmiler I built new Qt6.12.0 packages for js8lib for Mac, Windows, and Linux (both arm64 and x86_64). These are the only library packages we can't build on the runner because they don't have enough disk space. I built all four of them on a M4 Pro processor, emulating a ARMv8 for the linux aarch64 one, and emulating a 12th gen Intel Alder Lake for the two x86_64 ones. Mainly because the Core i7 emulation is really slow, for whatever reason. Both of the linux builds done on Ubuntu 24 Server. I wasn't totally sure what processor to use for the x86_64 builds, if I should go with the older Core i7 models or the Alder Lake one. This is the first time I've used the Alder Lake emulation. That Alder Lake processor is like 5 years old now, so I THINK it will be ok. After I get the new libs tested on my personal repo I'll upload them to the js8lib assets. Qt 6.12 is the new LTS release, good for five years. I've been running the beta's since day one and there is no known issues with 6.12.0 in our code. I suspect they'll have 3 point releases like they always do. Those are bug-fix only, stuff that usually don't apply to our application. We'll evaluate that when the time comes. We're no longer supporting Intel Mac after v3.0.3. The Homebrew guys dropped it. Apple no longer supports it. Wife walked off with my Intel MacBook Pro, said I don't need it no more, took it to the Apple Store and traded it on a MacBook Neo. I'm jumping on the bandwagon and I'm not supporting it either. All my Universal build scripts are now historical artifacts since they don't work with MacOS 27 anyway. I might look at them a year from now and go, "wow, what a pain that was...." and then they'll get deleted. |





Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
@wmiler @punk-kaos @Joe-K0OG @rruchte @aknrdureegaesr @Aqueum @Beiri22 @bazineta
This is a complete re-design of the JS8Call user interface that I started working on a couple months ago in my spare time, and finally got it functional today. It changes both the horizontal and vertical QSplitters to get rid of the full-width header bar with the button grid. That was largely wasted screen space. Plus that jack-of-all-trades-master-of-none Mode Button had like 15 call sites that ran on every UI tick just to update its text display, and that caused all the UI elements in that header bar to jump to the side every time it changed. What an expensive disaster (from a code standpoint)! Whoever dreamed that up had a nightmare and documented it in code.
So I consolidated all the UI controls into a dual-row layout by assembling a new Control Bar. It features both status labels and on/off buttons that are grey when turned off and green when turned on. It gets rid of the Tx status and last Tx labels because they were redundant. That information is already shown in the incoming MSG pane (textEditRX) and composition pane (extFreeTextMsgEdit) anyway. The on/off buttons feature an intelligent display where if you have a feature turned on in the settings, but a code function() momentarily disables it the button will turn a pale color instead of grey that indicates it's on, but disabled by the code function().
It gives your Call Activity table more vertical room since the header bar no longer takes up that space. And it shows at the top what you have your current expire time set to for the table before callsigns disappear out of it.
The old status bar is totally gone, no longer useful. The very infrequent status messages will display in the Control Bar on the left side by displacing the labels and control buttons to the right.
Example of HB ACK being turned on, but HB networking is turned off - this mimics the context menu's behavior where it will show as being checked but greyed out:
The Mode Button has a menu when clicked that is identical to the context menu items.
The Call Activity table matches the height of the MSG Inbox when you have it docked on the right:
The Tune button turns red when pressed:
These button behaviors are easily settable, per platform, in styles.h. If the linux guys want 1980's GTK buttons with their Tune button turning to Hot Pink, and the Windows guys want it to turn Dirt Brown with Blaze Orange text when pressed, while the Mac guys have a red one, that is configurable as the default in styles.h.
I gotta tune the size of the QSplitter handles and a few other things yet. Code not available at this point - I've never pushed the branch to my personal repo yet.
All reactions