Archive for the 'Code and Cruft' Category

What’s Wrong With This Dialog?

Sunday, May 16th, 2010

Sometimes I can’t help myself. This week, I read a blog post asking what was wrong with a dialog. I found plenty of stuff wrong, so I decided to write a response and create a replacement dialog. This is not intended as an attack on the dialog author. I hope it’s not received that way. Rather, I just felt an itch to answer the question posted with my own opinion on the subject.

Read on for the gory details.

Deciding what’s wrong

When I find myself asking if a feature is “wrong”, the first thing I do is ask if the feature should even exist. That is especially important with dialogs. Dialogs interrupt a user’s work flow, and for whatever non-deterministic-human-psychological reason, in-line GUI elements tend to interrupt humans less than pop-up dialogs. I try to avoid dialogs, and pop-ups, almost religiously. So when I find myself asking, “what’s wrong with this dialog?”, the next question I ask is “should this dialog even exist?”. Unfortunately, in this article, I can’t tell whether that’s the case, so I’ll assume it must exist.

The litany

In my view, this dialog contains many mistakes. Its core functionality is certainly accessible, but it’s not as easy as it should be. Here’s my list of mistakes:

Now I’ll try to explain myself in prose rather than pixels.

Mistake 1: Too many buttons

This dialog provides a simple CRUD user interface, but it has 6 buttons. That’s too many buttons for such a simple task

Mistake 2: Wasted space

Don’t get me wrong. I’m a fan of liberal white space between elements. I think it puts users at ease, when used properly. But in this case, there is too much wasted space.

Mistake 3: Misalignment

The 6 buttons all have icons, but none of the buttons or text align vertically. This forces the human eye to read more carefully, causing unnecessary stress.

Mistake 4: No way to revert

If I make a mistake with this dialog, my only option is to manually revert my changes. Sometimes this approach is okay, especially with simple dialogs like this one, but as a user, I find it comforting when there is a “cancel” button to rescue me from my own fat fingers. I realize that the GNOME project usually follows the “apply-as-you-type” paradigm, but I prefer the OK/Cancel/Apply approach.

Mistake 5: Editing, Deleting, and Re-ordering is too hard

Although the “focus-and-click” method for editing, deleting, and re-ordering has seen a lot of use over the years, I find it dated and confusing to users. I much prefer to inline the “edit” and “remove” buttons as close to their respective elements as possible, reducing confusion about exactly what will be deleted when I click a button. Also, clicking “Up” and “Down” to move the focused item in the list is old school. Much better to drag and drop in modern user interfaces.

The Proposal

If I woke up in the UI designer’s shoes tomorrow, and this dialog was my number one priority, I would probably refactor it as follows:

Improvement 1: Easy editing

The actions of the “Edit” and “Remove” buttons are super obvious. To edit an item, click the “Edit” button right on it. No ambiguity. You also don’t have to worry about graying out buttons when there is no selection.

Improvement 2: Drag and drop

To re-order items in the list, just drag ‘em.

Improvement 3: Cancel

If you don’t like your changes, click “Cancel”. Also, if you want to see the effect of your changes without closing the dialog, click “Apply”. Very handy if you want to try out lots of little changes without having to re-open the dialog between each change.

Improvement 4: No focusing

Now that the “Edit” and “Remove” buttons are in-line with the list items, there’s no need for a selection/focus model at all. That’s one less thing the user has to worry about.

Improvement 5: Simpler layout

The layout is essentially vertical. The user sees two areas, one above the other, and the two areas do only one thing each. The top area contains the list of editable items. The bottom area deals with saving your changes (the “add” button is a small exception). I think this design is more approachable and easier for a user to understand quickly.

Conclusion

So that’s what I propose. A simpler, cleaner approach using modern methods to make the editing task easier on users. It doesn’t make them think about the user interface, but rather they are freed to think about the task they want to accomplish with the user interface.

Smart Folder Synchronization with Python

Wednesday, May 12th, 2010

I have various files that download to my computer automatically. They arrive at different times of the day or night. When they do, I like to transfer them to a network drive on another computer automatically.

But there’s a wrinkle.

When the files auto-download to my computer, they go into one folder automatically, but when I transfer them to my network drive, I like them to go into different folders, based on their file name.

Python to the rescue.

I whipped out this little script that runs from cron every 5 minutes on my Mac, rsync’ing files from my “Downloads” folder into a specific subfolder of “/Volumes/Shared”, following a set of rules. In this script, any file with the string “dave” in its name gets copied to the “/Volumes/Shared/Dave Stuff” folder. Any file with “bob” in its name gets sent to “/Volumes/Shared/Files for Bob”, and so on.

Here it is for your enjoyment:

#!/usr/bin/python

# Filename filters, and which folders to send them to:
filters = {
    # Filename  :  Dest Folder
    'dave'      : 'Dave Stuff',
    'bob'       : 'Files For Bob',
    'frank'     : 'Franks Junk',
    }

src   = '/Users/Dave/Downloads'
dest  = '/Volumes/Shared'
rsync = 'rsync --times '

# ----------------------------------------------------------

import os;
import sys;
import subprocess;

# Only show progress when we're running in a terminal (and not cron):
if sys.stdout.isatty():
    rsync = rsync + '--progress '

for dir, dirs, files in os.walk(src):
    for filename in files:
        if filename.startswith(".") or filename.endswith(".part"):
            continue
        fullpath = os.path.join(dir, filename)
        for filter, destfolder in filters.iteritems():
            if filename.lower().find(filter) >= 0:
                fulldest = os.path.join(dest, destfolder)
                print "Copying '" + filename + "' to folder '" + destfolder + "'"
                cmd = rsync + ' "' + fullpath + '" "' + fulldest + '/."'
                process = subprocess.Popen(cmd, shell=True)
                try:
                    process.wait()
                except KeyboardInterrupt:
                    process.kill()
                    sys.exit(1)
                break
        else:
            print 'Could not find a home for file "' + filename + '"'

When this script runs, it just blindly tells rsync to transfer the files, but rsync will only transfer them if they are newer in the “src” folder than the “dest” folder. That’s thanks to rsync’s “–times” argument, which tells rsync to preserve the file times when it does the transfer.

So far it’s working great. I called it “sync-download-files” and created a crontab file called /Users/Dave/etc/crontab, that looks like this:

*/5 * * * * ~/bin/sync-download-files >/dev/null 2>&1

Then I ran this crontab command to install the job:

crontab /Users/Dave/etc/crontab

And voila! Files now auto-sync to /Volumes/Shared every 5 minutes. When there are no files to sync, the script completes in a couple seconds.

By the way, in my case, /Volumes/Shared is a Samba mounted network share to a WDTV Live box.

OpenWrt gets Qt SQL Support

Sunday, April 18th, 2010

On US tax day 2010, OpenWrt’s “qt4″ feed got support for MySQL and SQLite. And there was much rejoicing among Qt developers.

A big thanks to Mirko Vogt for porting the build system to OpenWrt and letting me help out with the SQL stuff.

Using scp between Linux and Mac OS X

Sunday, April 11th, 2010

For a while, I haven’t been able to get scp to work between Linux and Mac OS X. This only happened when I used the scp command on my Linux box, with Mac OS X acting as the ssh server. Here’s the symptom:

On Linux:

$ scp user@192.168.1.5:~/Documents/Some/File.txt /tmp/.
Password:
$

It disconnected me after entering my password without transferring the file. In the Gnome File Browser (nautilus), if I tried to connect to the Mac OS X box using ssh:// in the address bar, the gvfsd-sftp process would consume 100% of the CPU until I killed it.

Here’s the solution:

On the Mac OS X computer, in my ~/.bashrc file, I was echo‘ing some text just to say that my .bashrc had been sourced. Removing that echo line fixed the problem and now both the scp command and the Gnome File Browser work just fine.

Reminder: Don’t Hard Code It

Tuesday, April 6th, 2010

Today’s reminder comes from the “magic number” department, where we remind you to never hard-code a magic number in your software. For example, the value for ETIMEDOUT on Linux can vary depending on your C library. glibc, for example, defines it as 110, but uClibc defines it as 145. This tends to matter when you are using a return value from, for example, libusb, at an important branching point in your software. Of course, this whole exercise is hypothetical. I would never hard code a number like that, of course not.

The upside:
The libusb source code is available for the world to see.

The downside:
The libusb documentation makes no mention of ETIMEDOUT.

Never tell me the odds!

Thursday, March 25th, 2010

M&Ms are yummy. At work we have ginormous bags of M&Ms to snack on. One day I reached into the bag and pulled out a handful (about 8 M&Ms). I happily munched until there were 4 left. Lo and behold! The remaining 4 were all blue. “What are the odds,” I asked myself. So I set out to find the answer.

I started by figuring out how many different M&M colors there were. My bag had 6:

  • red
  • green
  • blue
  • brown
  • yellow
  • orange

Then I assumed that there were an equal number of each color in the bag (that may not actually be true, but it makes the problem a tad easier to solve).

Armed with this information, I started up the wayback machine to go back to Computer Science 235, when, back in my college days, I studied probability. This is when I realized that I had forgotten pretty much everything I knew about probability, except dice rolling odds (which I use in Risk to conquer the world). So it turned out that memory lane wasn’t that helpful after all.

At this point I decided that I should compute the odds of choosing a handful of 8 M&Ms at random with at least 4 blue ones, this being a prerequisite of having 4 blue ones left in my hand after eating the other 4. To do this, I first drew 8 boxes on my white board, to represent the 8 M&Ms I could draw at random. This helped me reason about the problem. For any given box, I have a 1 in 6 chance of drawing a blue M&M. If I draw a blue one for a given box, that leaves 3 more I would need to also randomly draw. So I started enumerating a few permutations that could make this happen:

1 2 3 4 5 6 7 8

And so on. I realized pretty quick that there were too many permutations to enumerate so it was time to get analytical. I started to reason this way:

In position 1, I have a 1 in 6 chance of choosing blue, but I don’t really care which position the blue M&M lands in. I also know that, if I get lucky and chose blue in position 1, that does not impact my odds of choosing blue in position 2. So each position is independent. What that means is I have a 1 in 6 chance of choosing blue the first time, and again the second time, and the third time, and the fourth time. But, and this is the kicker, I don’t have to choose blue for the first 4. It just has to be any 4.

So if I were to compute the odds of choosing all 8 blue, it would look something like this:

1/6 x 1/6 x 1/6 x 1/6 x 1/6 x 1/6 x 1/6 x 1/6.

That’s a good starting point. Since we don’t have to choose all 8, but only 4, blue M&Ms, we can just throw out 4 of the terms, so we end up with this:

1/6 x 1/6 x 1/6 x 1/6

This is when I started getting confused. I started to wonder if maybe the 8 don’t have anything to do with the problem at all. Maybe the odds of choosing 8 at random, eating 4, and being left with only blue are the same odds as just randomly choosing 4 blue M&Ms.

This is when I ditched the analytical in favor of the empirical. As much as I wanted to eat 10,000 M&Ms to prove my hypothesis, I decided to write a small computer program to randomly choose 8 M&Ms from a virtual bag, randomly eat 4 of the virtual M&Ms, and then see if the remaining M&Ms were all blue. This I did, and ran it 5,000,000 times. The result: 0.077% of the time, I had 4 blue M&Ms left. That’s right, 0.077% is the same as 1/6 x 1/6 x 1/6 x 1/6. So I conclude that the odds are the same, and the problems are, in fact, equivalent.

So I beat the odds that day when I had 4 blue M&Ms left. Without even trying, I did something whose odds were 1,296 to 1, against. I should have gone to Vegas that afternoon.

P.S. The odds of having 4 M&Ms left that are all the same color (any color) are 6 times easier: 1 in 216.

P.P.S. If you want to see the source code (warning: it’s C++), just ask.

How to auto-reboot your Comcast cable modem

Tuesday, March 16th, 2010

My office has a piece-of-junk Comcast cable modem that often needs to be rebooted. I got tired of walking back to the network closet, so I bought a USB relay for $30.

Yeah, I could have simply bought a better modem, but where’s the fun in that?

Here’s a photo of the relay board:

After connecting it to the modem’s power, I wrapped it in heat shrink:

And its final resting place where it will happily power cycle my cable modem for its entire useful life:

I put the relay between the modem’s power adapter and the modem itself, and connected the relay to my Linux server’s USB port for control. Once connected, the Linux kernel happily registered it as /dev/ttyUSB0 (it uses an FTDI chip for its USB-to-Serial translation, which is supported by all modern Linux kernels).

When connecting the power, I used the “C” and “NC” ports (“C” = “Common” and “NC” = “Normally Closed”). I didn’t use the “NO” port (“NO = “Normally Open”) because I don’t want my modem powering off just because the relay loses power (e.g., when the Linux box reboots).

This simple shell script power cycles the modem, which I run nightly using cron:


#!/bin/bash
device=/dev/ttyUSB0
printf '\xFF\x01\x01' >$device # Power off
sleep 60
printf '\xFF\x01\x00' >$device # Power on

I noticed that I needed to let the modem sleep for longer than 10 seconds, or there are problems on the network. In particular, our Outlook clients seem to lose their brains to our (off-site) Exchange server unless I sleep for longer. I used 60 seconds just to be safe.

The next step is to setup another cron job on my Linux box that will power cycle the modem if it ever discovers that it can’t reach the internet, or that its internet bandwidth is degraded.

I also wrote a small C++ program for Windows (about 20 lines) just to test the relay. It worked fine, but Linux was a lot easier to setup (no compiler necessary). In the time it took me to write that stupid C++ program, I was able to create both a working Python script and working shell script to do the same job (and Linux didn’t even require me to know the baud rate).

By the way, I would have avoided this whole exercise if my modem, like many others, had a software controllable reboot (some can be rebooted with wget, but not mine).

Happy (hardware) hacking!

Fancy QSlider Stylesheet

Wednesday, March 10th, 2010

Seriously. I don’t know how I ever created a Qt user interface without using stylesheets. This was, by far, the best idea Trolltech ever had.

For today’s show-and-tell, I give you a nifty-looking QSlider with some nice gradient style applied to it:

The handle has a nice hover effect, the bar has a moving gradient as you slide the slider, and it looks good when disabled. Notice also the rounded corners. No image files were harmed in the making of this widget.

Here’s the stylesheet code (I used Qt’s example as a starting point).

QSlider::groove:horizontal {
border: 1px solid #bbb;
background: white;
height: 10px;
border-radius: 4px;
}

QSlider::sub-page:horizontal {
background: qlineargradient(x1: 0, y1: 0,    x2: 0, y2: 1,
    stop: 0 #66e, stop: 1 #bbf);
background: qlineargradient(x1: 0, y1: 0.2, x2: 1, y2: 1,
    stop: 0 #bbf, stop: 1 #55f);
border: 1px solid #777;
height: 10px;
border-radius: 4px;
}

QSlider::add-page:horizontal {
background: #fff;
border: 1px solid #777;
height: 10px;
border-radius: 4px;
}

QSlider::handle:horizontal {
background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
    stop:0 #eee, stop:1 #ccc);
border: 1px solid #777;
width: 13px;
margin-top: -2px;
margin-bottom: -2px;
border-radius: 4px;
}

QSlider::handle:horizontal:hover {
background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
    stop:0 #fff, stop:1 #ddd);
border: 1px solid #444;
border-radius: 4px;
}

QSlider::sub-page:horizontal:disabled {
background: #bbb;
border-color: #999;
}

QSlider::add-page:horizontal:disabled {
background: #eee;
border-color: #999;
}

QSlider::handle:horizontal:disabled {
background: #eee;
border: 1px solid #aaa;
border-radius: 4px;
}

Enjoy!

Qt Layout Squashing

Tuesday, March 9th, 2010

I’ve encountered this problem a handful of times over the past few years using Qt’s layouts. Qt generally does a great job making sure that your Windows cannot be resized too small such that your content would be truncated. However, it seems that introducing a QStackedWidget causes a problem.

These two screenshots illustrate the issue:

Screenshot 1 The widget prior to resizing (all text appears correctly)

Screenshot 2 The user resizes the widget (the text is truncated)

Like I said above, I think the problem is caused by the QStackedWidget. Without it, Qt properly limits the widget’s minimum size such that the text cannot be truncated. I would love it if the Trolls could have a look at this issue. I’ve seen the problem since Qt 4.0, but I never reduced the problem to its base (it took a while to deduce that the QStackedWidget was the culprit).

If you’re interested, I’ve included the minimal Designer UI file that shows the problem (right click that link to save it). I used Designer 4.6.2 to create this file.

Talking to Qt Threads

Sunday, February 7th, 2010

I wrote about multi-threading in Qt a while back, and how to use QThread to wrap a blocking function call “lock free”. Today we’ll talk about how to pass data into your thread. This approach can be used, for example, to tell your QThread to stop.

There are two ways to use QThread, with and without an event loop, and the preferred method for talking to a QThread depends on which of them you use.

Way 1: Without an event loop

When you’re not using an event loop, the thread’s run() method often looks like this:

void MyThread::run()
{
    forever
    {
        // Do some stuff
    }
}

Since this method does not use an event loop, there is no way to deliver a signal to your thread. So if you want to pass data into the thread, you have to use a good old fashioned mutex. Let’s look at an example showing you how to stop your thread:

void MyThread::run()
{
    forever
    {
        {
            // "mutex" and "stopRequested" are member
            // variables of MyThread:
            QMutexLocker locker(&mutex);
            if(stopRequested)
                return;
        }

        // Do some stuff
    }
}

void MyThread::stop()
{
    QMutexLocker locker(&mutex);
    stopRequested = true;
}

In this case, we have to check the stopRequested variable in a timely manner in our thread’s run() method. The longer you run between checks, the longer it will take your thread to actually stop.

Outside observers can use the finished() signal to know when your thread is actually done. So if you are in a QMainWindow, for example, and a closeEvent() happens, you can ignore the event, call MyThread::stop(), and then when the QThread::finished() signal arrives, you can actually close the window.

The downside is that the stop() call will actually block while it tries to acquire the mutex. Given the way this code is written, the blocking will probably be very short, but hey, I hate blocking. Let’s see if we can dig up a better way to do this.

Way 2: With an event loop

If you have an event loop, you can use Qt’s meta-objects to talk to your thread. Let’s look at the same example as before, only this time with no locking or blocking.

void MyThread::MyThread()
{
    moveToThread(this);
}

void MyThread::run()
{
    QTimer *timer = new QTimer();
    connect(timer, SIGNAL(timeout()),
        this, SLOT(doSomething()));
    timer->start(0);

    exec(); // starts the event loop, and doesn't
            // return until it is told to quit()
}

void MyThread::stop()
{
    if(currentThread() != this)
    {
        // The caller is running in a
        // different thread, so use Qt to
        // call stop() later, on our own thread:
        QMetaObject::invokeMethod(this, "stop",
                        Qt::QueuedConnection);
    }
    else
    {
        // Now the call has arrived from our
        // own thread, yay! We can safely
        // shut down our event loop.
        quit();
    }
}

Look mom! No locks! Now we have killed our thread, safely and gracefully. There is no chance of blocking, and we learned something about QMetaObject.

A couple items to note:

  • The doSomething() method is left as an exercise for the reader, but be careful about the QTimer interval. I used 0, which means it will be firing almost constantly.
  • The stop() method must be a slot in MyThread (and not just a regular method) or else invokeMethod() will return false and not actually re-invoke stop() for you.
  • You can pass arguments to your thread this way, but it requires a bit more fun with QMetaObject::invokeMethod().
  • You can reduce this whole thing to a magical macro that you could put at the top of your stop() method, saving you from having to write if(currentThread() == this) at the top of every method. Hint: use the __FUNCTION__ macro.
  • To run this example code, you’ll need to #include these files: QThread, QTimer, QMetaObject, QMutexLocker, and QMutex
  • To call quit(), it may not actually be necessary to be running on the same QThread (it works for me without the QMetaObject), but this will be required when you start passing in data to your thread. Without it, your program could do unpredictable naughty things. I can’t find anything in the docs about whether quit() is thread safe.

I’ve found this QMetaObject approach the most effective and easiest way to pass data to QThreads safely, without blocking and without locks.

Happy threading!