Thursday, October 20, 2011

Nexsys: NavigationWidget Slots

We've started blocking in our interface pretty well - we're at least getting some of what we'd expect for a file browsing application - namely, we can see files.

But now we really have to get in there and start working with connecting up all the blocked in actions to some slots to do the real work.



Navigation Slots

The first slots we'll create on our NavigationWidget are some navigation slots.  We want to let the user navigate the folder system using the actions we've setup and the keyboard.  We want them to be able to navigate into a folder, back up to its parent window, all the way up to the root path, as well as to the user's home directory.

So, we're going to make a series of go methods.

When I add methods to my classes, I always try to add them alphabetically.  This can REALLY help when navigating a file, especially when you're naming methods consistently, you'll know where to look for your boolean, search, setter methods.  It'll also become easier for another developer coming into your code to contribute new methods because they'll know where they should go to add them, versus just blindly appending to the end of a class.  (This has led to so many duplicated methods in my time....)

So, with that in mind, lets add the following methods between the currentPath and setCurrentPath methods:

    def goHome( self ):
        """
        Goes to the home path for the current user.
        """
        self.setCurrentPath(QtCore.QDir.homePath())
    
    def goToRoot( self ):
        """
        Goes to the root path for the whole system.
        """
        self.setCurrentPath('')
    
    def goUp( self ):
        """
        Goes up a level from the current path.
        """
        levels = os.path.normpath(self.currentPath()).split(os.path.sep)
        self.setCurrentPath(os.path.sep.join(levels[:-1]))

These three methods are pretty simple navigation methods.  They take advantage of the setCurrentPath method that we've already written to handle all the proper updates to the interface.

The goToRoot method is self-explanatory, as it simply sets the current path to a blank string, which will set the system to use the root path.

goUp Method

The goUp method is a little more complex.  What this method is doing, is taking our current path and splitting it into a list of items.  The os.path.sep variable will contain a platform specific definition of what a path separator is ('/' for Linux and MacOS, and a '\' for Windows).  There are actually platform independent methods for doing file/folder navigation in Qt as well, and would work just as well here.

    def goUp( self ):
        """
        Goes up a level from the current path.
        """
        levels = os.path.normpath(self.currentPath()).split(os.path.sep)
        self.setCurrentPath(os.path.sep.join(levels[:-1]))

My general thought process for this is to try to use Python where possible, so more Python developers can understand the code, whether or not they're familiar with Qt - unless after testing, a Qt's system is better or more efficient (there are some platform cases that work better via Qt's abstraction than Python's).

We then strip out the end folder, and rebuild the path to navigate to a parent directory.

goHome Method

The goHome method uses the Qt's QDir class to determine the home directory for the current user.  The QDir class provides a lot of useful platform-inspecific directory information that can help augment what is available from Python.

def goHome( self ): """ Goes to the home path for the current user. """ self.setCurrentPath(QtCore.QDir.homePath())
Update the setCurrentPath Method

In fact, lets actually use the QDir class to resolve a blank path as the default rootPath for the system.

Insert this code directly underneath your docstring for the setCurrentPath method:

        # resolve empty path to the root path
        if ( not path ):
            path = QtCore.QDir.rootPath()

This will re-direct a blank path to the default root directory using the QDir.rootPath method.

Note: If you're new to Python and/or C++, staticmethods are methods that can be called directly from a class level - they don't need an instance of the class to be called.  You can define staticmethods in Python using decorators, which we will use in a bit, but for now, this would be the difference between being able to call a method as QDir.rootPath() vs. needing to do: dir = QDir(); dir.rootPath()

Connect the Interface

The next step we need to do is connect the buttons we created in our NavigationWidget's ui file to our new methods, so add these two lines underneath your returnPressed connection:

        self.ui_goup_btn.clicked.connect(        self.goUp )
        self.ui_gohome_btn.clicked.connect(      self.goHome )

This will connect our 'ui_goup_btn' to our goUp slot when a user clicks it, and the 'ui_gohome_btn' to the goHome slot when clicked.

After all of this, go ahead and run the application and you can start to navigate around the filesystem a little bit. Your code should at this point look like:

#!/usr/bin/python ~workspace/nexsys/gui/navigationwidget.py

""" Defines the NavigationWidget class. """

# define authorship information
__authors__     = ['Eric Hulser']
__author__      = ','.join(__authors__)
__credits__     = []
__copyright__   = 'Copyright (c) 2011'
__license__     = 'GPL'

# maintanence information
__maintainer__  = 'Eric Hulser'
__email__       = 'eric.hulser@gmail.com'

import os.path

from PyQt4 import QtCore, QtGui

import nexsys.gui

class NavigationWidget(QtGui.QWidget):
    """ 
    Creates a reusable file navigation widget to move around between 
    different folders and files. 
    """
    
    def __init__( self, parent = None, path = '' ):
        super(NavigationWidget, self).__init__(parent)
        
        # load the ui
        nexsys.gui.loadUi( __file__, self )
        
        # create the main filesystem model
        self._model = QtGui.QFileSystemModel(self)
        self._model.setRootPath('')
        
        # determine the version of Qt to know if the QFileSystemModel is 
        # available for use yet
        if ( int(QtCore.QT_VERSION_STR.split('.')[1]) < 7 ):
            completer = QtGui.QCompleter(QtGui.QDirModel(self), self)
        else:
            completer = QtGui.QCompleter(self._model, self)
        
        # set the completion and model information
        self.ui_path_edit.setCompleter(completer)
        self.ui_contents_treev.setModel(self._model)
        
        # assign the default path
        self.setCurrentPath(path)
        
        # create the connections
        self.ui_path_edit.returnPressed.connect( self.applyCurrentPath )
        self.ui_goup_btn.clicked.connect(        self.goUp )
        self.ui_gohome_btn.clicked.connect(      self.goHome )
    
    def applyCurrentPath( self ):
        """
        Assigns the current path from the text edit as the current path \
        for the widget.
        """
        self.setCurrentPath( self.ui_path_edit.text() )
    
    def currentPath( self ):
        """
        Returns the current path that is assigned to this widget.
        
        :return     str
        """
        return str(self.ui_path_edit.text())
    
    def goHome( self ):
        """
        Goes to the home path for the current user.
        """
        self.setCurrentPath(QtCore.QDir.homePath())
    
    def goToRoot( self ):
        """
        Goes to the root path for the whole system.
        """
        self.setCurrentPath('')
    
    def goUp( self ):
        """
        Goes up a level from the current path.
        """
        levels = os.path.normpath(self.currentPath()).split(os.path.sep)
        self.setCurrentPath(os.path.sep.join(levels[:-1]))
    
    def setCurrentPath( self, path ):
        """
        Sets the current path for this widget to the inputed path, updating \
        the tree and line edit to reflect the new path.
        
        :param      path | str
        """
        # resolve empty path to the root path
        if ( not path ):
            path = QtCore.QDir.rootPath()
            
        # update the line edit to the latest path
        self.ui_path_edit.setText( os.path.normpath(str(path)) )
        
        # shift the treeview to display contents from the new path root
        index = self._model.index(path)
        self.ui_contents_treev.setRootIndex(index) 

Coming up Next

Now that we have a few navigation options in our individual widgets, we'll need to connect them up to the window's actions as well. To do this, we'll also need to develop our system of knowing exactly which navigation widget in our main window is the currently active one.

So we'll start putting that system in there and hooking up the higher level window navigation actions to our widget's slots.

No comments:

Post a Comment