Friday 8 April 2011

Hiding the MenuBar in a PyQt application.

I've long been a fan of the firefox plugin "tiny menu" where screen real estate is preserved by compressing the application's menubar down into a single menu button.

Here's one way to achieve the same effect in pyqt4.






 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
#! /usr/bin/env python
# -*- coding: utf-8 -*-
'''
A simple application with a shrinkable menu bar.
(similar functionality to firefox4)

At any point either the tiny menu or menubar should be visible.
Therefore if the toolbar is hidden, the menu will re-appear.

to experience this... either 

    click View>Tiny Menu or hit F11
'''

from PyQt4 import QtGui, QtCore

class DockableMenuBar(QtGui.QMenuBar):
    def __init__(self, parent=None):
        QtGui.QMenuBar.__init__(self, parent)
        
        self.toggleViewAction = QtGui.QAction(_("Tiny &Menu"), parent)
        self.toggleViewAction.setShortcut('f11')
        self.toggleViewAction.setCheckable(True)
        self.toggleViewAction.triggered.connect(self.toggle_visability)
        
        self.menu_view = QtGui.QMenu(_("&View"), self)
        self.menu_view.addAction(self.toggleViewAction)
        self.addMenu(self.menu_view)
        
        self._menu_button = None
        
    @property
    def menu_button(self):
        self._menu_button = QtGui.QToolButton()
        self._menu_button.setPopupMode(QtGui.QToolButton.InstantPopup)
        self._menu_button.setText(_("Menu"))
        self._menu_button.setMenu(self.mini_menu)        
        self._menu_button.setToolButtonStyle(
            QtCore.Qt.ToolButtonFollowStyle)
        return self._menu_button

    @property
    def mini_menu(self):
        self._mini_menu = QtGui.QMenu()
        for action in self.actions():
            self._mini_menu.addAction(action)
        return self._mini_menu
        
    def addViewOption(self, action):
        '''
        add an action to the 'view' category of the toolbar
        ''' 
        self.menu_view.addAction(action)
        
    def addMenu(self, *args):
        try:
            return self.insertMenu(self.actions()[0], *args)
        except IndexError:
            return QtGui.QMenuBar.addMenu(self, *args)
            
    def addAction(self, *args):
        try:
            return self.insertAction(self.actions()[0], *args)
        except IndexError:
            return QtGui.QMenuBar.addAction(self, *args)        

    def toggle_visability(self, set_visible):
        self.setVisible(not set_visible)
        if set_visible:
            self.emit(QtCore.SIGNAL("mini menu required"), self.menu_button)
        else:
            self.emit(QtCore.SIGNAL("hide mini menu"))
            
    def setNotVisible(self, menu_bar_visible):
        '''
        make sure that we don't end up with neither menu visible!
        ''' 
        if not menu_bar_visible:
            self.setVisible(True)
        
class DockAwareToolBar(QtGui.QToolBar):
    def __init__(self, parent=None):
        QtGui.QToolBar.__init__(self, parent)
        self.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)
        self.setObjectName("DockAwareToolbar") #for QSettings
        
        # this should happen by default IMO.
        self.toggleViewAction().setText(_("&ToolBar"))

    def add_mini_menu(self, menu_button):
        self._menu_button = menu_button
        if self.actions():
            self.insertWidget(self.actions()[0], menu_button)
        else:
            self.addWidget(menu_button)
        self.show()
    
    def clear_mini_menu(self):
        self._menu_button.hide()
        self._menu_button.setParent(None)
        self._menu_button.deleteLater()
        self._menu_button = None
              
class TestMainWindow(QtGui.QMainWindow):
    def __init__(self, parent=None):
        QtGui.QMainWindow.__init__(self, parent)
        
        ## initiate instances of our classes
        
        self.toolbar = DockAwareToolBar()        
        menu_bar = DockableMenuBar(self)
        
        ## the menu bar needs this action adding
        menu_bar.addViewOption(self.toolbar.toggleViewAction())
        
        ## add them to the app
        self.addToolBar(QtCore.Qt.TopToolBarArea, self.toolbar)
        self.setMenuBar(menu_bar)
        
        ## make them aware of one another
        self.connect(self.menuBar(), QtCore.SIGNAL("mini menu required"),
            self.toolbar.add_mini_menu)
        self.connect(self.menuBar(), QtCore.SIGNAL("hide mini menu"),
            self.toolbar.clear_mini_menu)
        self.toolbar.toggleViewAction().triggered.connect(
            self.menuBar().setNotVisible)
        
        ## some arbitrary stuff to make the app more realistic
        file_action = QtGui.QAction("&File", self)
        self.menuBar().addAction(file_action)
        
        line_edit = QtGui.QLineEdit("http://google.com")
        self.toolbar.addWidget(line_edit)
        
        go_but = QtGui.QPushButton("Go!")
        go_but.setFixedWidth(60)
        self.toolbar.addWidget(go_but)
        
        te = QtGui.QTextEdit()
        self.setCentralWidget(te)
        te.setText(__doc__)
        

if __name__ == "__main__":
    import gettext
    gettext.install("")
    
    app = QtGui.QApplication([])
    mw = TestMainWindow()
    mw.show()
    app.exec_()




Thursday 7 April 2011

Python Class Attributes. A Quiz!

So take a look at this code.

#! /usr/bin/env python
'''
A simple class demonstrating attributes
'''
ID_COUNTER = 0

class Person(object):
    genus = "homo sapien"

    def __init__(self, name, sex="M"):
        global ID_COUNTER
    
        assert sex in ("M", "F"), 'INVALID SEX "%s" must be "M" or "F"'% sex
        ID_COUNTER += 1
        self.id = ID_COUNTER
        self.name = name
        self._sex = sex
        self._profession = None

    @property
    def sex(self):
        return "Male" if self._sex=="M" else "Female"

    @property
    def profession(self):
        if self._profession is None:
           return "unknown"
        return self._profession

    def set_profession(self, profession):
        self._profession = profession

    def __repr__(self):
        return "Person %03d:\n\t%s\n\tGenus\t(%s)\n\t%s\n\t%s"% (
            self.id,
            self.name,
            self.genus,
            self.sex,
            self.profession)

if __name__ == "__main__":
    person1 = Person("Neil")
    person1.set_profession("dentist")

    person2 = Person("Joan", "F")
    person2.genus = "Neanderthal"

    person3 = Person("Timrit", "M")
    person3.set_profession("refrigeration")
    
    for object_ in (Person.genus, person1, person2, person3):
        print object_
        print

which gives the following output

homo sapien

Person 001:
 Neil
 Genus (homo sapien)
 Male
 dentist

Person 002:
 Joan
 Genus (Neanderthal)
 Female
 unknown

Person 003:
 Timrit
 Genus (homo sapien)
 Male
 refrigeration


And here are your questions.
1. Why is the Global statement used on line 11?
2. is there a better way of implementing a unique serial ID for these objects?
3. What would happen if I tried to create an instance with the following call?
person4 = Person("ArtV61", "unknown")
4. is genus a "class attribute" or an "instance attribute"?
5. what is the difference between a "class attribute" or an "instance attribute"?
6. is Person an old or new style class?
7. what would need to change in this code to make it run under python3?
8. what is the __repr__ function for, and what would be the output if it were deleted?
9. what namespace is the __name__ variable found in?
10. why is the trailing underscore used for object_ on the penultimate line of code?


Answers, as always to linc AT thelinuxlink.net, quoting "QUIZ" in the subject field.