Skip to main content

Ralsina.Me — Roberto Alsina's website

Posts about kde

A week using tiling windows.

It has been a lit­tle over a week since I com­mit­ted to us­ing a tiling win­dow man­ag­er.

Sure, I am cheat­ing be­cause I am ac­tu­al­ly still us­ing KDE plus Kröhnkite but my win­dows are tiled and I am lik­ing it a lot.

Why this and not i3 or what­ev­er? Be­cause I don't want to change my lifestyle, I just want my win­dows to not over­lap gen­er­al­ly.

Kröhnkite pro­vides enough tiling func­tion­al­i­ty that I get (I think) the ben­e­fits with­out the mas­sive up­heaval of giv­ing up ev­ery­thing I am used to in my desk­top. I still use the Win­dows Key (ok, ok, the "Meta" key) to launch app­s. I still have a plas­ma pan­el with plas­moids at the bot­tom of my mon­i­tor, I can still float the win­dows if I want to! I can still use most of the short­cuts from my past 24 years us­ing KDE (yes, re­al­ly) and so on.

What are some things I had to change to adap­t?

  • I had to change to fo­­cus-­­fol­lows-­­mouse. BUT for the first time since I start­ed us­ing FVWM in 1993 I am lik­ing fo­­cus-­­fol­lows-­­mouse bet­ter than click­­-­­to-­­fo­­cus. It turns out KDE's im­­ple­­men­­ta­­tion of it is quite nice and al­­most "does what I mean". As it says in the doc­s, "like click to fo­­cus, but just don't click­­".

  • I re­­moved win­­dow dec­o­ra­­tion­s. Yes, you can keep them, but they feel out of place.

  • I set thick­­er win­­dow bor­der­s. Re­­siz­ing win­­dows via short­­­cuts is just not nice in gen­er­al, so thick­­er bor­ders help.

What are some things I have liked?

  • Fixed tiling lay­out in one mon­i­­tor and float­ing in the oth­­er is awe­­some when need­ed. And I can get it in place with one key­­press! So, in gen­er­al, dy­­nam­ic, sep­a­rate lay­outs for each screen is very, very use­­ful.

  • Hav­ing a "til­ing" wm that still re­spects most WM con­ven­­tions is good. So, pop­ups float. Yay.

  • The Al­t+En­ter short­cut to make a win­dow the "im­por­tan­t" one is neat.

  • Love how max­i­miza­­­tion/min­i­miza­­­tion work­s.

What are some things I have not liked?

  • The "tiled" lay­out has mul­ti­­ple ver­­sions you can switch be­tween with Ctr­l+I/D ... and well, some­­times none of them is ex­ac­t­­ly what I wan­t? Al­­so, the high­­er num­bered ones on­­ly are use­­ful when you have many win­­dows tiling, and if you don't they don't do any­thing.

  • Since I have no win­dow dec­o­ra­tions, the bru­tal in­con­sis­ten­cy on ap­p-quit­ting short­cuts is an­noy­ing. It can be ctr­l+q or ctr­l+x or esc or what­ev­er. I end up do­ing al­t+f4 which feels like win­dows 3.11.

  • The UX of KWin scripts is a bit lack­­ing. I in­­stalled an­oth­er one a while ago, called Quar­ter-Til­ing, and I have re­­moved ev­ery trace of it from my sys­tem... ex­­cept for its short­­­cut­s, which will ap­­par­en­t­­ly pol­­lute my con­­fig di­alogs for­ev­er.

So, ex­per­i­ment will con­tin­ue!

(Sor­ry, the video is in span­ish)

PyQt Quickie: command line parsing

So, you are writ­ing a PyQt ap­p, and you want it to sup­port com­mand line ar­gu­ments. So you do some­thing like this:

opt_parser = OptionParser()
opt_parser.add_option("-q", dest="quickly", action="store_true",
    help="Do it quickly (default=False)")
(options, args) = opt_parser.parse_args(sys.argv)
app = QApplication(sys.argv)
:
:
:

Or maybe even QAp­pli­ca­tion([]). Ok, you are doing it wrong. And this is wrong in most tutorials, too. Why? Because Qt (and thus PyQt) supports a bunch of useful command line options already. So if you do it like in the first listing, and pass "-style=oxygen" or whatever, one of the following will happen.

  1. Op­t­­Pars­er is go­ing to tell you it's not a valid op­­tion and abort

  2. You will ig­nore the op­­tion and not do any­thing use­­ful with it

  3. You will have your own -style op­­tion and do two things with it

All three out­comes are less than ide­al.

The right way to do this is:

opt_parser = OptionParser()
opt_parser.add_option("-q", dest="quickly", action="store_true",
    help="Do it quickly (default=False)")
app = QApplication(sys.argv)
(options, args) = opt_parser.parse_args(app.arguments())
:
:
:

This way, you give PyQt a chance to process the options it recognizes, and, then, you get to handle the rest, because app.arguments() has all Qt options removed.

The bad side of this is, you will make --help slightly slower, since it will have to build a QApplication to do nothing, and you will have undocumented options. Solution for both problems left as an exercise.

Shipping your PyQt app for windows

I have writ­ten about this in the past, with the gen­er­al con­clu­sion be­ing "it's a pain in the as­s".

So, now, here is how it's done.

  1. Start with a work­ing PyQt ap­­pli­­ca­­tion. In this ex­am­­ple, I will use de­vi­­cen­­zo.py most­­ly be­­cause:

    1. It is a work­ing PyQt ap­­­pli­­­ca­­­tion.

    2. It us­es a big chunk of PyQt

    3. It's easy to test

  2. Now you need a set­up.py. Here's one that work­s, with ex­ten­sive com­m­ments.

# We will be using py2exe to build the binaries.
# You may use other tools, but I know this one.

from distutils.core import setup
import py2exe

# Now you need to pass arguments to setup
# windows is a list of scripts that have their own UI and
# thus don't need to run in a console.

setup(windows=['devicenzo.py'],
      options={

# And now, configure py2exe by passing more options;

          'py2exe': {

# This is magic: if you don't add these, your .exe may
# or may not work on older/newer versions of windows.

              "dll_excludes": [
                  "MSVCP90.dll",
                  "MSWSOCK.dll",
                  "mswsock.dll",
                  "powrprof.dll",
                  ],

# Py2exe will not figure out that you need these on its own.
# You may need one, the other, or both.

              'includes': [
                  'sip',
                  'PyQt4.QtNetwork',
                  ],

# Optional: make one big exe with everything in it, or
# a folder with many things in it. Your choice
#             'bundle_files': 1,
          }
      },

# Qt's dynamically loaded plugins and py2exe really don't
# get along.

data_files = [
            ('phonon_backend', [
                'C:\Python27\Lib\site-packages\PyQt4\plugins\phonon_backend\phonon_ds94.dll'
                ]),
            ('imageplugins', [
            'c:\Python27\lib\site-packages\PyQt4\plugins\imageformats\qgif4.dll',
            'c:\Python27\lib\site-packages\PyQt4\plugins\imageformats\qjpeg4.dll',
            'c:\Python27\lib\site-packages\PyQt4\plugins\imageformats\qsvg4.dll',
            ]),
],

# If you choose the bundle above, you may want to use this, too.
#     zipfile=None,
)
  1. Run python set­up.py py2exe and get a dist fold­er full of bi­na­ry good­ness.

And that's it. Ex­cept of course, that's not it.

What this will do is cre­ate a bi­na­ry set, ei­ther a fold­er full of things, or a sin­gle EXE file. And that's not enough. You have to con­sid­er at least the fol­low­ing:

  1. Put ev­ery­thing in re­­source files: im­ages, qss files, icon­s, etc. Ev­ery file your app need­s? Put it in a re­­source file and load it from there. That way you don't have to care about them if you go the "one ex­e" road.

  2. Com­pile .ui files to .py (same rea­­son)

  3. Fig­ure out if you use Qt's plu­g­in­s, and make them work. This in­­­cludes: us­ing Phonon, us­ing Qt­SQL, and us­ing any im­age for­­mats oth­­er than PNG.

Af­ter you have that, are you done? NO!

Your win­dows us­er will want an in­stall­er. I am not go­ing to go in­to de­tail­s, but I had a good time us­ing Bi­tRock­'s In­stall­Builder for Qt. It's a nice tool, and it work­s. That's a lot in this field.

But is that al­l? NO!

You have to take care of the Vis­ual Stu­dio Run­time. My sug­ges­tion? Get a copy of the 1.1MB vcre­dis­t_x86.exe (not the larg­er one, the 1.1MB one), and ei­ther tell peo­ple to in­stall it man­u­al­ly, or add it to your in­stall­er. You are legal­ly al­lowed (AFAIK) to re­dis­tribute that thing as a whole. But not what's in it (un­less you have a VS li­cense).

And we are done? NO!

Once you run your app "in­stalled", if it ev­er prints any­thing to stder­r, you will get ei­ther a di­a­log telling you it did, or worse (if you are in ay­thing new­er than XP), a di­a­log telling you it can't write to a log file, and the app will nev­er work again.

This is be­cause py2exe catch­es stderr and tries to save it on a log­file. Which it tries to cre­ate in the same fold­er as the bi­na­ry. Which is usu­al­ly not al­lowed be­cause of per­mis­sion­s.

So­lu­tion? Your app should nev­er write to stder­r. Write an ex­cepthook and catch that. And then re­move stderr or re­place it with a log file, or some­thing. Just don't let py2exe do it, be­cause the way py2exe does it is bro­ken.

And is that it?

Well, ba­si­cal­ly yes. Of course you should get 4 or 5 dif­fer­ent ver­sions of win­dows to test it on, but you are pret­ty much free to ship your app as you wish. Oh, mind you, don't up­load it to down­load­s.­com be­cause they will wrap your in­stall­er in a larg­er one that in­stalls bloat­ware and crap.

So, there you go.

De Vicenzo: A much cooler mini web browser.

It seems it was on­ly a few days ago that I start­ed this projec­t. Oh, wait, yes, it was just a few days ago!

If you don't want to read that again, the idea is to see just how much code is need­ed to turn Qt's We­bKit en­gine in­to a ful­ly-fledged brows­er.

To do that, I set my­self a com­plete­ly ar­bi­trary lim­it: 128 lines of code.

So, as of now, I de­clare it fea­ture-­com­plete.

The new fea­tures are:

  • Tabbed brows­ing (y­ou can ad­d/re­­move tab­s)

  • Book­­marks (y­ou can ad­d/re­­move them, and choose them from a drop-­­down menu)

This is what al­ready worked:

  • Zoom in (C­tr­l++)

  • Zoom out (C­tr­l+-)

  • Re­set Zoom (C­tr­l+=)

  • Find (C­tr­l+F)

  • Hide find (Esc)

  • But­­tons for back­­/­­for­ward and reload

  • URL en­try that match­es the page + au­­to­­com­­plete from his­­to­ry + smart en­try (adds http://, that kind of thing)

  • Plug­ins sup­­port (in­­clud­ing flash)

  • The win­­dow ti­­tle shows the page ti­­tle (with­­out brows­er ad­ver­tis­ing ;-)

  • Progress bar for page load­­ing

  • Sta­­tus­bar that shows hov­­ered links URL

  • Takes a URL on the com­­mand line, or opens http://python.org

  • Mul­ti­­plat­­form (works in any place QtWe­bKit work­s)

So... how much code was need­ed for this? 87 LINES OF CODE

Or if you want the PEP8-­com­pli­ant ver­sion, 115 LINES OF CODE.

Be­fore any­one says it: yes, I know the ren­der­ing en­gine and the tool­kit are huge. What I wrote is just the chrome around them, just like Aro­ra, Rekon­q, Ga­le­on, Epiphany and a bunch of oth­ers do.

It's sim­ple, min­i­mal­is­tic chrome, but it works pret­ty good, IMVHO.

Here it is in (bug­gy) ac­tion:

It's more or less fea­ture-­com­plete for what I ex­pect­ed to be achiev­able, but it still needs some fix­es.

You can see the code at it's own home page: http://de­vi­cen­zo.­google­code.­com

How much web browser can you put in 128 lines of code?

UP­DATE: If you read this and all you can say is "o­h, he's just em­bed­ding We­bKit", I have two things to tell you:

  1. Duh! Of course the 128 lines don't in­­­clude the ren­der­ing en­gine, or the TCP im­­ple­­men­­ta­­tion, or the GUI tool­k­it. This is about the rest of the browser, the part around the web ren­der­ing en­gine. You know, just like Aro­ra, Rekon­q, Epiphany, and ev­ery­one else that em­beds we­bkit or mozil­la does it? If you did­n't get that be­­fore this ex­­pla­­na­­tion... facepalm.

  2. Get your favourite we­bkit fork and try to do this much with the same amount of code. I dare you! I dou­ble dog dare you!

Now back to the orig­i­nal ar­ti­cle


To­day, be­cause of a IRC chat, I tried to find a 42-­line web brows­er I had writ­ten a while ago. Sad­ly, the paste­bin where I post­ed it was dead, so I learned a lesson: It's not a good idea to trust a paste­bin as code repos­i­to­ry

What I liked about that 42-­line brows­er was that it was not the typ­i­cal ex­am­ple, where some­one dumps a We­bkit view in a win­dow, loads a page and tries to con­vince you he's cool. That one is on­ly 7 lines of code:

import sys
from PyQt4 import QtGui,QtCore,QtWebKit
app=QtGui.QApplication(sys.argv)
wb=QtWebKit.QWebView()
wb.setUrl(QtCore.QUrl('http://www.python.org'))
wb.show()
sys.exit(app.exec_())

And if I want­ed to make the code uglier, it could be done in 6.

But any­way, that 42-­line brows­er ac­tu­al­ly looked use­ful!

This 42-line web browser, courtesy of #python and #qt -- http... on Twitpic

Those but­tons you see ac­tu­al­ly worked cor­rect­ly, en­abling and dis­abling at the right mo­men­t, the URL en­try changed when you clicked on links, and some oth­er bit­s.

So, I have de­cid­ed to start a smal­l, in­ter­mit­tent project of code golf: put as much brows­er as I can in 128 lines of code (not count­ing com­ments or blanks), start­ing with PyQt4.

This has a use­ful pur­pose: I al­ways sus­pect­ed that if you as­sumed PyQt was part of the base sys­tem, most apps would fit in flop­pies again. This one fits on a 1.44MB flop­py some 500 times (so you could use 360KB com­modore flop­pies if you prefer­!).

So far, I am at about 50 lines, and it has the fol­low­ing fea­tures:

  • Zoom in (C­tr­l++)

  • Zoom out (C­tr­l+-)

  • Re­set Zoom (C­tr­l+=)

  • Find (C­tr­l+F)

  • Hide find (Esc)

  • But­­tons for back­­/­­for­ward and reload

  • URL en­try that match­es the page + au­­to­­com­­plete from his­­to­ry + smart en­try (adds http://, that kind of thing)

  • Plug­ins sup­­port (in­­clud­ing flash)

  • The win­­dow ti­­tle shows the page ti­­tle (with­­out brows­er ad­ver­tis­ing ;-)

  • Progress bar for page load­­ing

  • Sta­­tus­bar that shows hov­­ered links URL

  • Takes a URL on the com­­mand line, or opens http://python.org

  • Mul­ti­­plat­­form (works in any place QtWe­bKit work­s)

Miss­ing are tabs and proxy sup­port. I ex­pect those will take an­oth­er 40 lines or so, but I think it's prob­a­bly the most fea­ture­ful of these toy browser­s.

The code... it's not all that hard. I am us­ing lamb­da a lot, and I am us­ing PyQt's key­word ar­gu­ments for sig­nal con­nec­tion which makes lines long, but not hard. It could be made much small­er!

Here it is in ac­tion:

And here's the code:

#!/usr/bin/env python
"A web browser that will never exceed 128 lines of code. (not counting blanks)"

import sys
from PyQt4 import QtGui,QtCore,QtWebKit

class MainWindow(QtGui.QMainWindow):
    def __init__(self, url):
        QtGui.QMainWindow.__init__(self)
        self.sb=self.statusBar()

        self.pbar = QtGui.QProgressBar()
        self.pbar.setMaximumWidth(120)
        self.wb=QtWebKit.QWebView(loadProgress = self.pbar.setValue, loadFinished = self.pbar.hide, loadStarted = self.pbar.show, titleChanged = self.setWindowTitle)
        self.setCentralWidget(self.wb)

        self.tb=self.addToolBar("Main Toolbar")
        for a in (QtWebKit.QWebPage.Back, QtWebKit.QWebPage.Forward, QtWebKit.QWebPage.Reload):
            self.tb.addAction(self.wb.pageAction(a))

        self.url = QtGui.QLineEdit(returnPressed = lambda:self.wb.setUrl(QtCore.QUrl.fromUserInput(self.url.text())))
        self.tb.addWidget(self.url)

        self.wb.urlChanged.connect(lambda u: self.url.setText(u.toString()))
        self.wb.urlChanged.connect(lambda: self.url.setCompleter(QtGui.QCompleter(QtCore.QStringList([QtCore.QString(i.url().toString()) for i in self.wb.history().items()]), caseSensitivity = QtCore.Qt.CaseInsensitive)))

        self.wb.statusBarMessage.connect(self.sb.showMessage)
        self.wb.page().linkHovered.connect(lambda l: self.sb.showMessage(l, 3000))

        self.search = QtGui.QLineEdit(returnPressed = lambda: self.wb.findText(self.search.text()))
        self.search.hide()
        self.showSearch = QtGui.QShortcut("Ctrl+F", self, activated = lambda: (self.search.show() , self.search.setFocus()))
        self.hideSearch = QtGui.QShortcut("Esc", self, activated = lambda: (self.search.hide(), self.wb.setFocus()))

        self.quit = QtGui.QShortcut("Ctrl+Q", self, activated = self.close)
        self.zoomIn = QtGui.QShortcut("Ctrl++", self, activated = lambda: self.wb.setZoomFactor(self.wb.zoomFactor()+.2))
        self.zoomOut = QtGui.QShortcut("Ctrl+-", self, activated = lambda: self.wb.setZoomFactor(self.wb.zoomFactor()-.2))
        self.zoomOne = QtGui.QShortcut("Ctrl+=", self, activated = lambda: self.wb.setZoomFactor(1))
        self.wb.settings().setAttribute(QtWebKit.QWebSettings.PluginsEnabled, True)

        self.sb.addPermanentWidget(self.search)
        self.sb.addPermanentWidget(self.pbar)
        self.wb.load(url)


if __name__ == "__main__":
    app=QtGui.QApplication(sys.argv)
    if len(sys.argv) > 1:
        url = QtCore.QUrl.fromUserInput(sys.argv[1])
    else:
        url = QtCore.QUrl('http://www.python.org')
    wb=MainWindow(url)
    wb.show()
    sys.exit(app.exec_())