A few days ago someone said something that reminded me about my audio player, which I had abandoned for more than a year already. The reason was mostly that the two Phonon backends, VLC and gstreamer, for some reason or other couldn't play the files I had without any gaps between songs.
To be honest, the first bug end up being me not properly encoding the filenames.
If you first URL-encoded the filename and then built a Q/KURL with that, then
it's all fine. It took me more than 12 months and a few rereads of the thread
to realize it. Fixes apart, it seems that the bug still exists for other
instances of gstreamer errors, so we're not out of the woods. In any case, I
switched to the VLC backend and it seems that now is able to fire the
aboutToFinnish() signal properly, so for the moment I'm using that.
All that is fine, but that's not what I wanted to talk about in this post. Given
that this project largely precedes my interest on testing, it has no testing at
all. Most of the project is straightforward enough to almost no need any, but
there's a critic part that would not suffer at all if it had any, namely the
Collections handling, including passing files from one to another and
automatically updating new/removed
So after fixing the bug mentioned above I tried to figure out the current state
of affaires regarding
Collections, and boy, they're in bad shape. The code was
locally modified, never commited, deactivating any notifications of filesystem
changes (new or removed files), and other code I can't really understand the
Because if this last detail is that I decided to start testing three classes:
Collection, which handles a set a
Songs with a common root directory;
CollectionAggregator, which handles a set of collections and should coordinate
Song from one
Collection to another; and
CollectionIndexer, which scans
Collection's root dir to find
All went fine while I tested the first class, Collection. There was a tricky
part where I had to setup a
QApplication in order to make signals work. The
problems began when I started testing
CollectionIndexer. Tests started blocking
endlessly, signals stopped being either emited or firing the connected slots,
life was bad.
I tried to search the available documentation and mailing lists for a hint about the problem, but besides a quite complex example that didn't seem to properly converge to anything useful, I was mostly on my own.
This morning I got my eureka moment: I noticed that if I executed each test class
by itself, it worked, but both at the same time blocked and never finished. Then
I remembered something said in
For any GUI application using Qt, there is precisely one QApplication object, no matter whether the application has 0, 1, 2 or more windows at any given time.
That was it: I was creating the application, first in the
setUp() method, then
as a class attribute, but I had one test class per class to test, each in its
own file. Somehow this last fact lead me to think that somehow they were executed
in separate processes, which is not true. Luckily, even with this limitation, there's
none on the amount of times you can
quit() the same instance, so
that's what I did: I created only one instance and reused it everywhere. I was
already doing that for each test method, but again, somehow having several files
mislead me to think they were isolated from each other.
So now all my unit tests work without mysteriously blocking forever. Now I just
hope I can keep riding the success wave and bring
satyr into good shape. A new
release wouldn't hurt.
 No matter how much I try, I can't get any vaguer.
 Ok, maybe the
Playlist combo wouldn't hurt to have UTs either.
After several months thinking about it, and just two requests, I finally decided
satyr's code. I decided to use github because I already switched to
git, mainly for testing and understanding it. I think I
can live with
hg, althought branch management in
git seems to be given more
thought and a good implementation.
So, without further ado: satyr in github
Remember, it's still a test software, by no means polished or ready for human
consumption, and with very low development force. Still, I think it has some
nice features, like interchangeable skins and a well defined backend,
support, quick tag edition, reasonable collection managment, and, thanks to
Phonon, almost-gapless playback and things like «stop after playing the
current this file» (but not «after any given file» yet).
In Debian Sid it mostly works only with the
GStreamer backend; I haven't tried
xine one and I know
VLC does not emit a signal needed for queueing the
next song, so you have to press «next» after each song. AFAIK this is fixed
satyr-0.3.2 "I should install my own food" is out. The Changelog is not very impressive:
- We can save the list of queued Songs on exit and load it at startup.
- The queue position is not presented when editing the title of a Song that is queued.
- Setting the track number works now (was a bug).
- Fixed a bug in setup.py.
but the last one is somewhat important (Thanks Chipaca). Also, 2 months ago, I made the satyr-0.3.1 "freudian slip" release, when user 'dglent' from http://kde-apps.org/ found a packaging bug. It was not only a bugfixing revision, it also included new features:
- nowPlaying(), exported via dbus.
- 'now playing' plugin for irssi.
- Changes in tags is copied to selected cells (in the same column). This allows 'Massive tag writing'.
- Fixed "NoneType has no attribute 'row'" bug, but this is just a workaround.
- Forgot to install complex.ui.
Now go get it!
So I mostly finished implementing a
QAbstractTableModel subclass with
read/write support, and some more stuff. Here's a resume of what changed from
the implementation of [a read-only
columnCount(), the first important difference occurs
data() itself. Now the
QModelIndex.column() is important, because it
tells us which column the view wants to populate. In the case of the
DisplayRole I ask the
Song for the corresponding atribute/key; in the
case of the
SizeHintRole I use a static array of strings to provide default
values. I will explain why later:
def data (self, modelIndex, role): if modelIndex.isValid () and modelIndex.row ()<self.aggr.count: song= self.aggr.songForIndex (modelIndex.row ()) if role==Qt.DisplayRole: attr= self.attrNames [modelIndex.column ()] data= QVariant (song[attr]) elif role==Qt.SizeHintRole: size= self.fontMetrics.size (Qt.TextSingleLine, self.columnWidths[modelIndex.column ()]) data= QVariant (size) else: data= QVariant () else: data= QVariant () return data
The second difference is related to the view itself. A
QTableView presents not
only the contents but also what they call 'headers'. These are the column
headers ans row numbers that normally appear on top and left of tables. By
default numbers are shown, which in may case is ok for rows, but not for
columns. So we implement
headerData(), which is somehow similar to
section parameter refers to the column when direction is
def headerData (self, section, direction, role=Qt.DisplayRole): if direction==Qt.Horizontal and role==Qt.DisplayRole: data= QVariant (self.headers[section]) else: data= QAbstractTableModel.headerData (self, section, direction, role) return data
Once I have a basic read-only table, I start nitpicking on its appearance. First, all the columns have the same width, which is not acceptable. I want narrower 'Year', 'Track' and 'Length' columns, wider 'Artist', 'Album' and 'Title' columns and a huge 'Filepath' column. So, we set the column widths from the ui:
class MainWindow (KMainWindow): [...] def connectUi (self, player): [...] # FIXME: kinda hacky self.fontMetrics= QFontMetrics (KGlobalSettings.generalFont ()) for i, w in enumerate (self.model.columnWidths): self.ui.songsList.setColumnWidth (i, self.fontMetrics.width (w)) class QPlayListModel (QAbstractTableModel): def __init__ (self, aggr=None, songs=None, parent=None): QAbstractTableModel.__init__ (self, parent) [...] # FIXME: hackish self.columnWidths= ("M"*15, "M"*4, "M"*20, "M"*3, "M"*25, "M"*5, "M"*100)
Now the rows, the rows! They're too thick! I can only see tweintysomething songs
in my 1440x900 screen with a small plasma panel on top, and I already got rid of
the empty menubar and the status bar.
headerData() is also called with a
SizeHintRole, so let's use it (once more, the third time already, using the
QFontMetrics). Note that it also sets the width; and while I'm on it,
I also align the row numbers to the right:
def headerData (self, section, direction, role=Qt.DisplayRole): if direction==Qt.Horizontal and role==Qt.DisplayRole: data= QVariant (self.headers[section]) elif direction==Qt.Vertical: if role==Qt.SizeHintRole: # again, hacky. 5 for enough witdh for 5 digits size= self.fontMetrics.size (Qt.TextSingleLine, "X"*5) data= QVariant (size) elif role==Qt.TextAlignmentRole: data= QVariant (Qt.AlignRight|Qt.AlignVCenter) else: data= QAbstractTableModel.headerData (self, section, direction, role) else: data= QAbstractTableModel.headerData (self, section, direction, role) return data
Now I can see 36 rows maximized and almost 39 when in full screen. Maybe I can make the columns headers thinner... but once more I digress.
Now to the interesting part: making the model read/write. First we must
implement a method called
flags() which says a lot of things about cells; in
particular, if they're editable or not. Clearly 'Length' and 'Filepath' are not
def flags (self, modelIndex): ans= QAbstractTableModel.flags (self, modelIndex) if modelIndex.column ()<5: # length or filepath are not editable ans|= Qt.ItemIsEditable return ans
setData() itself. It has a similar interface as
including the new value. It should return if it could successfully change the
data value, which in this case means if
Tagpy/Taglib could modify the tag.
Note that the role while changing the value is
def setData (self, modelIndex, variant, role=Qt.EditRole): # not length or filepath and editing if modelIndex.column ()<5 and role==Qt.EditRole: song= self.aggr.songForIndex (modelIndex.row ()) attr= self.attrNames[modelIndex.column ()] try: song[attr]= unicode (variant.toString ()) except : ans= False else: self.dataChanged.emit (modelIndex, modelIndex) ans= True else: ans= QAbstractTableModel.setData (self, modelIndex, variant, role) return ans
It's really that simple. Now we have a
QTableView and a
which are read/write. This means that
satyr can modify and save tags now,
which is one of my most wanted features.
One last detail: for the way
satyr has its
QTableView configured, one can
enter in edit mode only by typing something in a cell, but then there are two
columns which cannot be modified. If you type something while the cursor is in
one of these columns,
QTableView will call
match() for matches to the typed
letters. The default implementation basically searches through all the cells,
which means that
data() is called for all the
Songs, which finally implies
that all the tags are read from disk, which is expensive for making the simple
mistake of typing in a read-only column. As
satyr already has a search
feature, I fixed this 'misfeature' with this code:
def match (self, start, role, value, hits=1, flags=None): # when you press a key on an uneditable cell, QTableView tries to search # calling this function for matching. we already have a way for searching # and it loads the metadata of all the songs anyways # so we disable it by constantly returning an empty list return 
 I have my reasons to provide both interfaces.
 Plans for saving the state of the
satyr main window will include the
column widths, but that's another story.
 It's usefull for writing tags of Songs with no tags... and because I can!
 ... which might return in the future to show scanning process, but again I digress.
 Yes, another digression: at some point it will be possible to move the file to a filepath based on its tags; in other words, to arrange your music collection based on tags. Should not be too far in the future.
 Last digression: I will modify this behaviour as soon as I finish with this post.
I must be crazy or something. Inestead of spending my last friday in France
for this year partying in some bar I satyed home and produced another
satyr release. The ? Here, have some:
- (De)Queueing Songs with the keyboard, with visual feedback.
- Remembers window size.
- Current song highlighted not with selection but with real color changes.
- We can select Songs. Right now useful only for queuing several songs at the same time.
- Workaround a bug in with the .
- Show the filepaths as much as possible in the user's encoding.
- Hitting F2 in a cell edits its contents.
- Silightly cleaner interface: don't show so many 0's.
- bug with perv/next: weren't wrapping around.
Go get it from the Download area! I promise to party double tomorrorw...
Continuing with the development of
Satyr, which doesn't have any GUI. I
thought it would be faster to make a
DBus interface that a good GUI, not
to mention more interesting.
The code snippet of today is this:
class DBusServer (dbus.service.Object): def __init__ (self): bus= dbus.SessionBus () bus_name= dbus.service.BusName ('org.kde.satyr.dbus.test', bus=bus) dbus.service.Object.__init__ (self, bus_name, "/server") @dbus.service.method('org.kde.satyr.dbus.test', in_signature='', out_signature='') def bing (self): print "bing!" @dbus.service.method('org.kde.satyr.dbus.test', in_signature='', out_signature='') def quit (self): app.quit () dbus.mainloop.qt.DBusQtMainLoop (set_as_default=True) dbs= DBusServer () sys.exit (app.exec_ ())
This simply defines a class which registers itself with the session bus
under the name
org.kde.satyr.dbus.test, exporting itself under the path
/server and then defining a method that goes bing! :) and another one
that quits the app. Note the decorator for the methods.
You might notice the
call. This is needed because both Qt and DBus in asyncronous mode (which is
the one we're using and the only one that works under Qt or Gtk, AFAIK) both
have their own event loops, and that makes some kind of magic that let both
loops coexist without blocking the other. This must be called before
connecting to the bus; otherwise, you get this error:
<span class="createlink">RuntimeError</span>: To make asynchronous calls, receive signals or export objects, D-Bus connections must be attached to a main loop by passing mainloop=... to the constructor or calling dbus.set_default_main_loop(...)
So, let's test the beast. We run the script in one terminal and in the other:
mdione@mustang:~/src/projects/satyr/live$ qdbus | grep satyr org.kde.satyr.py-10154 org.kde.satyr.dbus.test mdione@mustang:~/src/projects/satyr/live$ qdbus org.kde.satyr.dbus.test / /server mdione@mustang:~/src/projects/satyr/live$ qdbus org.kde.satyr.dbus.test /server method void org.kde.satyr.dbus.test.bing() method QString org.freedesktop.DBus.Introspectable.Introspect() mdione@mustang:~/src/projects/satyr/live$ qdbus org.kde.satyr.dbus.test /server org.kde.satyr.dbus.test.bing mdione@mustang:~/src/projects/satyr/live$ qdbus org.kde.satyr.dbus.test /server org.kde.satyr.dbus.test.quit
And in the other console:
mdione@mustang:~/src/projects/satyr/live$ python dbus_test.py bing!
It goes bing!... and then finishes. The next step is to make my
class to export its methods via
DBus and that's it!. More info in the
I figured several things after the last/first release. One of those is that one can't try to pull a beta of your first releases. Betas are for well stablished pieces of code which are supposed to be rock solid; initial releases not. Another thing I figured out (or actually remembered) is that old saying: release early, release often.
So instead of a
0.1 'official' release, where all the bugs are nailed down in
their coffins and everything is as peachy and rock solid as a peachy huge rock
(like the Mount Fitz Roy,
for instance), and only 13 days later than the initial release, we get another
satyr-0.2, codenamed "I love when two branches come
together", is out.
This time we got that pharaonic refactoring I mentioned in the last release, which means that skins are very independient from the rest of the app, which is good for skins developers and the core developers, even if those sets are equal and only contain me.
From the user point of view, the complex skin is nicer to see (column widths and headers, OMG!) and it also allows tag editing. Yes, because we have tag editing! Right now the only way to fire the edition is to start typing, which will erase previous data, but don't worry, I plan to nail that soon. At least it's usefull for filling up new tags. I also fixed the bug which prevented this skin to highlight which is being played. Lastly but not leastly, the complex skin has a seek bar, and the code got tons of cleanups.
So, that's it. It's out, go grab it!
 Right now I would consider
satyr just a small peeble in a highway, only
noticeable if some huge truck picks it up with its wheels and throws it to your
windshield. But I plan to reach at least to be a sizable rock such as that one
found near one of the Vikings in Mars.
satyr got the possibility to queue songs for playing very early. At that
moment there wasn't any GUI, so the only way to (de)queue songs was via
dbus. Once the GUI was there, we had to provide a way to queue songs. As
satyr aims to be fully usable only using the keyboard, the obvious way was to
setup some shortcut for the action.
Qt and then
KDE have a very nice API for defining 'actions' that can be
fired by the user. The ways to fire them include a shortcut, a menu entry or a
button in a toolbar. I decided to go with the
According to [the
classKAction.html#_details), to create an action is just a matter of creating
the action and to add it to an
actionCollection(). The problem is that nowhere
it says where this collection comes from. There's the
but creating one and adding the actions to it seems to be not enough.
If you instead follow the
see that it refers to a
KXmlGuiWindow, which I revised when I was desperately
looking for the misterious
actionCollection(). I don't know why, but the
documentation generated by
PyKDE does not include that method. All in all, the
tutorial code works, so I just ported my
class MainWindow (KXmlGuiWindow): def __init__ (self, parent=None): KXmlGuiWindow.__init__ (self, parent) uipath= __file__[:__file__.rfind ('.')]+'.ui' UIMainWindow, _= uic.loadUiType (uipath) self.ui= UIMainWindow () self.ui.setupUi (self) [...] self.ac= KActionCollection (self) actions.create (self, self.actionCollection ())
def create (parent, ac): '''here be common actions for satyr skins''' queueAction= KAction (parent) queueAction.setShortcut (Qt.CTRL + Qt.Key_Q) ac.addAction ("queue", queueAction) # FIXME? if we don't do this then the skin will be able to choose whether it # wants the action or not. with this it will still be able to do it, but via # implementing empty slots queueAction.triggered.connect (parent.queue)
Very simple, really. But then the action doesn't work! I tried different approaches, but none worked.
The tutorial I mentioned talks about some capabilities of the
one of them, the ability to have the inclusion of actions in menues and
toolbars dumped and loaded from XML file (hence, the XML part of the class
name), and that is handled by the
setupGUI() method. From its documentation:
«Configures the current windows and its actions in the typical
[...] Typically this function replaces
createGUI()». In my case the GUI is
already built by that
self.ui.setupUi() that you see up there, so I ignored
this method. But the thing is that if you don't call this method, the actions
will not be properly hooked, so they don't work; hence, my problem. But just
calling it make the actions work! I'll check later what's the magic behind this
method. For now just adding
self.setupGUI() at the end of the
So, that's it with actions. As a result I also get several things: a populated menu bar with Settings and Help options (but no File; that'll come later with standard actions, of which I'll talk about later, I think), with free report bug, about satyr and configure shortcuts options, among others. The later does work but its state is not saved. That also will come in the same post that standard actions.
PD: First post from my new internet connection. Will
satyr suffer from this?
Only time will tell...
 then you had to pick the index of the songs either by guessing or looking at the dump of the playlist.
More than two months ago I globed about QStrings and
The problem was this: my app accepts paths via command line, which are
KCmdLineOptions; which in turn converts everything to
QStrings. What I wanted were paths, which are more like
QStrings (because the latter have internally an unicode representation;
more on that later). Including
PyQt4 in the equation forced me to resort
QByteArray to get the path as a
str instead of using
PyQt4 doesn't export that function). But that's
only the beginning of the problem.
Take for instance this situation. I have a music collection that I've been
building for years now (more that 10, I think). In the old times of this
collection the filenames were encoded in
iso-8859-1. Then the future came
and converted all my machines to
utf-8. But only the software; the
filesystems were in one way or another inherited from system to system, from
machine to machine. So I ended with a mixture of utf and iso filenames, to
the point where I have a file whose filename is in iso, but the directory
where it is is in utf. Yes, I know, it is a mess. But if I take any decent
media player, I can play the file allright. That's because the filesystem
knows nothing of encodings (otherwise it would reject badly encoded
I just spent last saturday making sure that
satyr only stored filepaths in
QStrings. It took concentration, but having just
a bunch of classes and only 3 or 4 points where the filepaths are managed it
wasn't that difficult. Still, it took a day. But then, as I mentioned in
Phonon the is not able to play such files... or so I thought.
If you run
satyr after executing
export PHONON_XINE_DEBUG=1 you'll see a
Phonon debug info in the console (not that there is another way to
satyr right now anyways). Among all that info you'll see lines such as
void Phonon::Xine::XineStream::setMrl(const QByteArray&, Phonon::Xine::XineStream::StateForNewMrl) ... bool Phonon::Xine::XineStream::xineOpen(Phonon::State) xine_open succeeded for m_mrl = ...
If you're sharp enough (I'm not; sandsmark from
#phonon had to tell me)
you'll note the mention of MRL's. MRL's are
xine's URL for media. As any
URL, they can (and most of the time must) encode 'strange' characters with
the so-called "percent encoding". This means that no matter what encodings
the different parts of a filepath is in, I just add
file:// at the
beginning and then I can safely encode it scaping non-ascii characters to
%xx representations... or that's what the theory says. One thing to note is
file:// part must not be scaped;
xine complains that the file
does not exist in that case.
Looking for help in
Qt's classes one can find
QUrl and the already known
QByteArray. I can call
QByteArray.toPercentEnconding() from my
feed that to
QUrl.fromPercentEncoding() (which strangely returns a
QString, which is exactly what we're avoiding) or
But then the first function encodes too much, replacing
%3A%2F%2F. No fun.
Ok, let's try creating a
QByteArray with only the
file:// and then
toPercentEncoding() of the path only. It works:
But then calling
<span class="createlink">PyQt4</span>.QtCore.QUrl("file://xn--/home/mdione.../ltimo bondi a finisterre/07- la pequea novia del carioca-wkmz60758d.wav")
The URL got somehow puny-encoded,
which of course
xine doesn't recognize for local files.
Another option is to create an empty
QUrl.StrictMode so we avoid 50 lines of code that start
that try to escape everything all over again (and I already had some
double-or-even-triple-enconding nightmares parsing RSS/Atom feeds last year,
thank you), but we get puny-encoded again (maybe it is 'pwny-encoded'?).
Last resort: backtrack to the point were we created only one
with the path and call
toPercentEncoding(); feed that to the method
setEncodedPath() of an empty
QUrl. Then we add the last piece calling
setScheme('file') and we're ready! Of course we're not:
Notice the lack of the two
xine doesn't like it;
hence, I don't either.
Ok, this post got too long. I hope I can resolve this soon, I already spent too much time on it. At least a good part of it was expaining it, so others don't have to suffer the same as I did.
satyr will shortly be released, whether I fix this bug or not.
 Look at the size of that file! 6k lines to handle URL's! Who would say it was so difficult... Once more I'm remembered of how lucky I am to have this libraries at the tips of my fingers, yay!
A couple of days ago Marcelo Fernández wrote a simple image viewer in
less than 200 lines long, and I thought that it would be nice to compare how
the same app would be written in
PyKDE4. But then I though that it would not
be fair, as
KDE is a whole desktop environment and
GTK is 'only' a widget
library, so I did it in
To make this even more fair, I hadn't had a good look at the code itself, I only run it to see what it looks like: a window with only the shown image in it, both scrollbars, no menu or statusbar, and no external file, so I assume he builds the ui 'by hand'. He mentions these features:
- Pan the image with the mouse.
- F1 to F5 handle the zoom from 'fit to window', 25%, 50%, 75% and 100%.
- Zooming with the mouse wheel doesn't work.
Here's my take:
#! /usr/bin/python # -*- coding: utf-8 -*- # - Example image viewer in # Marcos Dione <email@example.com> - http://grulicueva.homelinux.net/~mdione/glob/ # TODO: # * add licence! (GPLv2 or later) from .QtGui import QApplication, QMainWindow, QGraphicsView, QGraphicsScene from .QtGui import QPixmap, QGraphicsPixmapItem, QAction, QKeySequence import sys class OMITGraphicsView (QGraphicsView): def __init__ (self, pixmap, scene, parent, *args): QGraphicsView.__init__ (self, scene) self.zoomLevel= 1.0 self.win= parent self.img= pixmap self.setupActions () def setupActions (self): # a factory to the right! zoomfit= QAction (self) zoomfit.setShortcuts ([QKeySequence.fromString ('F1')]) zoomfit.triggered.connect (self.zoomfit) self.addAction (zoomfit) zoom25= QAction (self) zoom25.setShortcuts ([QKeySequence.fromString ('F2')]) zoom25.triggered.connect (self.zoom25) self.addAction (zoom25) zoom50= QAction (self) zoom50.setShortcuts ([QKeySequence.fromString ('F3')]) zoom50.triggered.connect (self.zoom50) self.addAction (zoom50) zoom75= QAction (self) zoom75.setShortcuts ([QKeySequence.fromString ('F4')]) zoom75.triggered.connect (self.zoom75) self.addAction (zoom75) zoom100= QAction (self) zoom100.setShortcuts ([QKeySequence.fromString ('F5')]) zoom100.triggered.connect (self.zoom100) self.addAction (zoom100) def zoomfit (self, *ignore): winSize= self.size () imgSize= self.img.size () print winSize, imgSize hZoom= 1.0*winSize.width ()/imgSize.width () vZoom= 1.0*winSize.height ()/imgSize.height () zoomLevel= min (hZoom, vZoom) print zoomLevel self.zoomTo (zoomLevel) def zoom25 (self, *ignore): self.zoomTo (0.25) def zoom50 (self, *ignore): self.zoomTo (0.5) def zoom75 (self, *ignore): self.zoomTo (0.75) def zoom100 (self, *ignore): self.zoomTo (1.0) def zoomTo (self, zoomLevel): scale= zoomLevel/self.zoomLevel print "scaling", scale self.scale (scale, scale) self.zoomLevel= zoomLevel if __name__=='__main__': # this code is enough for loading an image and show it! app= QApplication (sys.argv) win= QMainWindow () pixmap= QPixmap (sys.argv) qgpi= QGraphicsPixmapItem (pixmap) scene= QGraphicsScene () scene.addItem (qgpi) view= OMITGraphicsView (pixmap, scene, win) view.setDragMode (QGraphicsView.ScrollHandDrag) view.show() app.exec_ () # up to here! # end
Things to note:
- The code for loading, showing the image and pan support is only 13 lines of
Pythoncode, including 3 imports. The resulting app is also able to handle vector graphics, but of course I didn't exploit that, I just added a
- Zooming is implemented via
QGraphicsView.scale(), which is accumulative (scaling twice to 0.5 actually scales to 0.25 of the original size), so I have to keep the zoom level all the time. There should be a
- The code for calculating the scale level is not very good: scaling between 75% and 50% or 25% produces scales of 0.666 and 0.333, which I think at the end of the day will accumulate a lot of error.
- For the same reason,
zoomToFit()has to do some magic. I also got hit by the integer division of
Python(I was getting zoom factors of 0) so I had to add
1.0*to the claculations. It's good that this is fixed in
- The size reported by the
QMainWindowwas any vegetable (it said 640x480 when it actually was 960x600), so I used the
- For some strange reason
zoomToFit()scales the image a little bigger than it should, so a scrollbar appears in the direction of the constraining dimension.
- Less that 100 lines! Even if
setupActions()could surely be improved.
- In Marcelo's favor I should mention that he writes docstrings for most of his methods both in english and spanish (yes, of course I read his code after I finished mine). I barely put a couple of comments, but doing the same should add 10 more lines, tops. Also, I don't want to convert this into a who-has-it-smaller contest (the code, I mean :).
- It took me approx 3 hours, with no previous knowledge of how to do it and no internet connection, so no asking around. I just used the «Qt Reference Documentation», going to the «Gropued Classes» page and to the «Graphics View Classes» from there.
- It doesn't zoom with the mouse wheel either.
- The default colors of
formatplugin are at most sucky, but better than nothing.
 Unluckly he didn't declared which license it has, so I'm not sure if I really can do this. I GPL'ed mine.