I’ve been planning on adding tab drag-and-drop functionality to VMware Workstation 6.0. Rather than implementing this ability from scratch (which I did with Gaim, and would rather not do again), I took the sane approach and started investigating the GTK+ 2.10 API for GtkNotebook tab drag-and-drop.
This is functionality that many applications have had to implement themselves, so it’s great that support had finally gone into GTK+ 2.10. So I must wonder, with all the various applications that would benefit from this new API, how did we get it so wrong?
Now, before I continue, let me say that I applaud the effort in getting this into GTK+ in the first place. Reordering tabs looks smooth, it’s only one API call, and the basics are trivial. Where this all falls down is when you try to do anything complicated with it, and by complicated I mean anything beyond a simple text editor.
Before starting, I investigated how many projects were using this API. A quick Google Code search shows that almost nobody does, aside from maybe the tab reordering API. I did find this list of complaints, which I remember reading before. I won’t repeat everything on this list, but I will list what I’ve ran into, and how I think we can improve this API.
Global functions are very bad. (Bug 386935)
In order to allow for a tab to be dragged out and form a new window, the application must call gtk_notebook_set_window_creation_hook and pass a callback function. When a tab marked as detachable is dragged to the root window, this function will be called. It is expected that the function create a new window, position it as per the x and y coordinates of the drop (if it so desires), show it, and return the resulting notebook. GTK+ will then add the tab to that notebook.
While useful, this suffers from a major design flaw. You can’t set a window creation hook per-notebook. You get exactly one window creation hook function, which must be responsible for any and all notebooks in the program. The hook function can only distinguish between them using the notebook’s group ID.
For smaller programs, this isn’t a huge limitation. Simple text editors and the like only need one function. However, imagine if your application has multiple notebooks that each need to be dragged, and imagine if the code for those notebooks are in two separate parts of the tree. Maybe you have a nice separation of the different parts of the project. Regardless, now you have to have one common function that knows about both and handles their window creations.
The problem gets far worse for applications separated into different libraries, or those using widget utility libraries. Two separate libraries both providing a notebook with drag-and-drop with their own window creation won’t be able to set up a hook. They would require that the main application handle determining the group IDs of the notebook widgets they care about and then calling the proper functions in the libraries. While doable, it’s a horrible burden on the application, and it doesn’t always work.
Of course, the whole thing completely breaks down when you’re writing a plugin with a notebook rather than an application. The plugin won’t be able to offer its own window creation, due to possible conflicts with the main application and other plugins.
The solution, of course, is to have per-notebook window creation hooks. GTK+ could attempt to call one of these and then fall back to the global hook, if it exists. If calling the global hook, GTK+ could spit out a warning informing the user that the program should be upgraded to the new API and that the existing method is deprecated.
Rather than functions, though, a signal handler may make more sense. It could use a collector and call the handlers until it finds one that returns a GtkNotebook. I could see this being useful if a widget component library (as part of a larger project) provides a default window creation handler that the calling application wants to override for a specific case.
Numeric group IDs lead to namespace collisions. (Bug 386930)
Right now, in order to indicate which notebooks are compatible for drag-and-drop operations, each GtkNotebook gets a group ID. This is an unsigned integer with absolutely no rules on how an ID should be picked. This is very dangerous, as it could cause namespace collisions in larger applications, resulting in tabs being droppable onto incompatible notebooks. This could easily crash such applications.
There’s no reason for us to be using integers. Take a look at GtkRadioButton. They also have groups, but they work a bit differently. The first GtkRadioButton defines the group, and the rest get passed that as the group identifier. In gtkmm, you actually have a Gtk::RadioButtonGroup object that you simply instantiate and then pass to each radio button.
Now, in any well-designed program, there should be only one place creating the notebook for a certain type of window, and usually that’s the only type of notebook capable of accepting tabs from the same class of notebook. So, why not do something like what gtkmm does and have some sort of static object that represents that group, define it once, and pass it to each notebook? This is guaranteed to be unique, and solves the namespace collision issue.
Signals need to be more clear. (Bug 386943)
You can determine if a page was added to a notebook or removed from it, but there’s no clear way of determining if it was due to an API call, or a drag-and-drop operation. We worked around it in VMware Workstation, but it would have been very helpful to know precisely that a page was added due to drag-and-drop. Same with the removed signal. I know they wanted to condense the signals and figured it would work in all cases, but it doesn’t, so please, give us some more specific signals!
Drop operations should be able to be programatically rejected. (Bug 386950)
There are times when you want to allow a tab to be draggable, but want to reject it in notebooks under certain circumstances. For example, in VMware Workstation, we have the “Home” tab. I would like to be able to drag this to empty windows, but if that window has a “Home” tab already, I want to reject the drag. To my knowledge, there is no way to do this currently.
Detaching tabs into new windows requires a drag to the desktop. (Bug 360225)
In most any program with tab drag-and-drop, you can drag a tab off into any area not in the tab bar and it will detach into a new window. With the GTK+ tab dragging, you have to actually drag it to the root desktop. Even dragging off into another window isn’t good enough. This sucks. Let’s fix this properly. We’re not doing anybody any favors.
I’m missing a few annoyances I ran into, but I’ll be blogging about them separately once I remember.
So, to recap, I believe we should:
- Deprecate gtk_notebook_set_window_creation_hook and add a new create_window signal, returning a GtkNotebook.
- Ditch the numeric group IDs and use some sort of identifier object or generic pointer to a static variable and pass that in instead.
- Add new signals or something telling us specifically how the tab was added/removed.
- Provide a way to reject a tab drop on a particular notebook programatically.
- Call the window creation hook any time the tab is dragged off the tab portion of the notebook.
It would be great to see these things fixed so that more applications can actually use this API without major headaches. Anyone up for the task?
I plan to put up a new post soon giving a couple of tips for using the current API.
Update: Bugs have been filed for the above. They’re in the topic headers.
Nice analysis but unfotunately completely useless.
If you think your blog is the proper place to discuss Gtk+ development you are seriously mistaken.
Posted to http://mail.gnome.org/archives/gtk-devel-list/ this could make a difference though.
> It would be great to see these things fixed so that more applications can actually use this API without major headaches. Anyone up for the task?
Yes, you. Seriously.
Don’t expect the rest of the crowd to fix the problems you seem to be very good at analyzing and presenting solutions for.
Make an effort and submit patches if you really want to see these things fixed, free software projects live from contributions, not requests.
I don’t get it, why are people always so quick to criticize? Christian’s presenting some legitimate problems here and trying to start a discussion — you can bet that bugs will be filed, and (time permitting) patches written. Given his record in the open-source community, I’m astounded at how many anonymous commenters are quick to point out the things he’s doing wrong, while very few people take time to laud him for his extensive contributions.
Free software projects live from amity and friendship, not anonymous chiding.
ShortComment: You seem to be under the mistaken impression that my blog post was the only action I am taking in order to improve this. You also appear to believe that I do not contribute patches to projects or know where the proper development mailing lists are. This is embarrassing to yourself.
My blog is syndicated on a number of Planets and many people read it. Many developers discuss ideas and problems on their blogs. It’s common practice, and not something that needs to be criticized, especially with that attitude.
I don’t always have the time to write every patch I want, because quite frankly, my time is taken up working on commercial software that a lot of GNOME developers use and also contributing to other GNOME software. Yes, some people who request things don’t make any effort to help on any projects, but that’s not me. The people who are reading this post that would be beneficial to this discussion already know that.
For the record, since you seem to be confused as to my knowledge of how open source projects live and how things get done, I was one of the main developers on Gaim, I’m the lead developer on the Galago presence project (used by Telepathy and the Nokia 770), the Desktop Notifications spec and libnotify/notification-daemon projects (used in most Linux distros these days), libsexy (used in a variety of projects), Leaftag (a tagging architecture a few of us have been working on), libview (a collection of useful widgets I helped open source from the VMware Workstation code), and have submitted patches to a variety of other projects over the years.
Thanks for your opinion on my lack of effort in the open source community, but research before you accuse. I am more than happy to work with people on improving this API. The first step is to get some discussion going, and believe it or not, that doesn’t always take place solely on listservs. And posts such as yours do nothing but damage these discussions.