Mozarella Ashbadger
A tabbed web-browser in Python, using PyQt

Mozarella Ashbadger is the latest revolution in web browsing! Go back and forward! Print! Save files! Get help! (you'll need it). Any similarity to other browsers is entirely coincidental.

Mozarella Ashbadger (Tabbed)

This is an updated version of the basic PyQt-based browser Mooseache which adds support for tabbed browsing. If you want to read about how a Qt web browser is implemented internals, read that first — this is just about tabifying it.

The full source code for Mozzarella Ashbadger is available in the 15 minute apps repository. You can download/clone to get a working copy, then install requirements using:

pip3 install -r requirements.txt

You can then run Mozzarella Ashbadger with:

python3 browser_tabbed.py

Read on for a walkthrough of how to convert the existing browser code to support tabbed browsing.

Creating a QTabWidget

Adding a tabbed interface to our browser is simple using a QTabWidget. This provides a simple container for multiple widgets (in our case QWebEngineView widgets) with a built-in tabbed interface for switching between them.

Two customisations we use here are .setDocumentMode(True) which provides a Safari-like interface on Mac, and .setTabsClosable(True) which allows the user to close the tabs in the application.

We also connect QTabWidget signals tabBarDoubleClicked, currentChanged and tabCloseRequested to custom slot methods to handle these behaviours.

    self.tabs = QTabWidget()
    self.tabs.setDocumentMode(True)
    self.tabs.tabBarDoubleClicked.connect( self.tab_open_doubleclick )
    self.tabs.currentChanged.connect( self.current_tab_changed )
    self.tabs.setTabsClosable(True)
    self.tabs.tabCloseRequested.connect( self.close_current_tab )

    self.setCentralWidget(self.tabs)

The three slot methods accept an i (index) parameter which indicates which tab the signal resulted from (in order).

We use a double-click on an empty space in the tab bar (represented by an index of -1 to trigger creation of a new tab. For removing a tab, we use the index directly to remove the widget (and so the tab), with a simple check to ensure there are at least 2 tabs — closing the last tab would leave you unable to open a new one.

The current_tab_changed handler uses a self.tabs.currentWidget() construct to access the widget (QWebEngineView browser) of the currently active tab, and then uses this to get the URL of the current page. This same construct is used throughout the source for the tabbed browser, as a simple way to interact with the current browser view.

def tab_open_doubleclick(self, i):
    if i == -1: # No tab under the click
        self.add_new_tab()

def current_tab_changed(self, i):
    qurl = self.tabs.currentWidget().url()
    self.update_urlbar( qurl, self.tabs.currentWidget() )
    self.update_title( self.tabs.currentWidget() )

def close_current_tab(self, i):
    if self.tabs.count() < 2:
        return

    self.tabs.removeTab(i)

The code for adding a new tab is is follows:

def add_new_tab(self, qurl=None, label="Blank"):

    if qurl is None:
        qurl = QUrl('')

    browser = QWebEngineView()
    browser.setUrl( qurl )
    i = self.tabs.addTab(browser, label)

    self.tabs.setCurrentIndex(i)

Signal & Slot changes

While the setup of the QTabWidget and associated signals is simple, things get a little trickier in the browser slot methods.

Whereas before we had a single QWebEngineView now there are multiple views, all with their own signals. If signals for hidden tabs are handled things will get all mixed up. For example, the slot handling a loadCompleted signal must check that the source view is in a visible tab and only act if it is.

We can do this using a little trick for sending additional data with signals. Below is an example of doing this when creating a new QWebEngineView in the add_new_tab function.

    # More difficult! We only want to update the url when it's from the
    # correct tab
    browser.urlChanged.connect( lambda qurl, browser=browser:
        self.update_urlbar(qurl, browser) )

    browser.loadFinished.connect( lambda _, i=i, browser=browser:
        self.tabs.setTabText(i, browser.page().title()) )

As you can see, we set a lambda as the slot for the urlChanged signal, accepting the qurl parameter that is sent by this signal. We add the recently created browser object to pass into the update_urlbar function.

Now, whenever the urlChanged signal fires update_urlbar will receive both the new URL and the browser it came from. In the slot method we can then check to ensure that the source of the signal matches the currently visible browser — if not, we simply discard the signal.

def update_urlbar(self, q, browser=None):

    if browser != self.tabs.currentWidget():
        # If this signal is not from the current tab, ignore
        return

    if q.scheme() == 'https':
        # Secure padlock icon
        self.httpsicon.setPixmap( QPixmap( os.path.join('icons','lock-ssl.png') ) )

    else:
        # Insecure padlock icon
        self.httpsicon.setPixmap( QPixmap( os.path.join('icons','lock-nossl.png') ) )

    self.urlbar.setText( q.toString() )
    self.urlbar.setCursorPosition(0)

This same technique is used to handle all other signals which we can receive from web views, and which need to be redirected. See the source code for Mozzarella Ashbadger for more examples.

Continue reading

Moonsweeper  gui

Explore the mysterious moon of Q'tee without getting too close to the alien natives! Moonsweeper is a single-player puzzle video game. The objective of the game is to explore the area around your landed space rocket, without coming too close to the deadly B'ug aliens. Your trusty tricounter ... More

Discussion