Creating a collapsible sidebar in GTK. Uses the following widgets: Gtk.Revealer, Gtk.Paned

Sidebars are great. I have recently been working on a tool named sysdot, which is useful for viewing directed graph relationships, using graphviz as its backend. While working on it, I felt the need for creating a sidebar. Unfortunately, python documentation of GTK is not very great. Even looking at the original C documentaion, I could not find a simple sidebar app. So here is my attempt at it.

First the initialising code:


import gi
gi.require_version('Gtk', '3.0')

from gi.repository import Gtk

class AppWindow(Gtk.ApplicationWindow):
	def __init__(self, *args, **kwargs):
		super().__init__(*args, **kwargs)
		# content in actual applications will likely be a box or grid container
		# but here it's fine for it to be a dummy label
		content = Gtk.Label(label="Dummy label")
		self.add(label)
		

class Application(Gtk.Application):
	def __init__(self, *args, **kwargs):
		super().__init__(*args, **kwargs)
		self.window = None
		
	def do_activate(self):
		if not self.window:
			self.window = AppWindow(application=self, title="Main Window")
		self.window.show_all()
		self.window.present()


if __name__ == '__main__':
	app = Application()
	app.run(sys.argv)

The above code will create a simple window, without any frills. We will add our stuff in the AppWindow class.

Next step is creating a sidebar. Now, there are many widgets that can be used to create a sidebar. You can use a Revealer, or Expander, or even a simple container like a box or grid. I went with a Revealer, since it already had animations built-in. If I had used a container widget, I would have to write my own animations.

So I create a sidebar class, and add it to the AppWindow.


class Sidebar(Gtk.Revealer):
    def __init__(self, nodes=None, edges=None, widget=None):
        Gtk.Revealer.__init__(self)
		tabs = Gtk.Notebook.new()
		
		tab = Gtk.ScrolledWindow.new()
		tabs.append_page(tab, Gtk.Label(label="First tab"))
		self.add(tabs)
		self.set_reveal_child(True)
		self.show_all()
		
class AppWindow(Gtk.ApplicationWindow):
	def __init__(self, *args, **kwargs):
		super().__init__(*args, **kwargs)
		# content in actual applications will likely be a box or grid container
		# but here it's fine for it to be a dummy label
		content = Gtk.Label(label="Dummy label")
		self.add(label)
		
		sidebar = Sidebar()
        self.add(sidebar)

For adding content to the sidebar, I used a notebook widget. You can also skip the content and directly add the scrolled window to the sidebar, or you can first add a box, and then add a stackSidebar and a stack sidebar widget. The important restriction is, Revealer can only contain one child. So if you find yourself needing multiple widgets, simply make the top level widget a container.

If we try to run the above code, it will show a warning, and won't show any sidebar. Reason is that the ApplicationWindow class is supposed to have only one child, like revealer (they both inherit from Gtk.Bin). So, to rectify that, we add both the content and sidebar in a more general purpose container, like a box or a grid. You can also use Gtk.Paned, which I will do shortly.


class AppWindow(Gtk.ApplicationWindow):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        box = Gtk.HBox()

        # content widget, here just a simple "Hello World" label
        sidebar = Sidebar()
        box.add(sidebar)

        content = Gtk.Label(label="Hello World")
        box.add(content)

        self.add(box)
        self.set_size_request(200, 200)

Cool, so now we have a pretty ugly looking sidebar, which is not resizeable, and which cannot be hidden, but is a sidebar nonetheless.

Next step is making the sidebar resizeable. For that, instead of messing around with the Sidebar class, I added a HPaned widget in the AppWindow, and added the sidebar to it.


class AppWindow(Gtk.ApplicationWindow):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        box = Gtk.HBox()
		
        pane = Gtk.HPaned(wide_handle=True) # makes for a better separator
        box.add(pane)
		
        sidebar = Sidebar()
        pane.pack1(sidebar, False, True)

        # content widget, here just a simple "Hello World" label
        content = Gtk.Label(label="Hello World")
        pane.pack2(content, True, False)

        self.add(box)
        self.set_size_request(200, 200)

Now we can resize the sidebar. Next step is the visibility.

First, I create a simple method in the Sidebar class, called toggleVisibility, which relies on get_child_revealed function of Revealer.


class Sidebar(Gtk.Revealer):
	...
	def toggleVisibility(self):
        if self.get_child_revealed():
            self.set_reveal_child(False)
            self.set_visible(False)
        else:
            self.set_reveal_child(True)
            self.set_visible(True)

Next, we can either call this method directly, or hook it up with a signal. Here, I am going with the former approach. I am going to replace my content label widget with a button widget, which on clicked, will call the sidebar's toggle method.


class AppWindow(Gtk.ApplicationWindow):
	...
        content = Gtk.Button(label="Hello World")
        content.connect("clicked", lambda x: sidebar.toggleVisibility())
        pane.pack2(content, True, False)
	...

And there we have it, a working sidebar.

For reference, here's the full code:


import gi
gi.require_version('Gtk', '3.0')

from gi.repository import Gtk

class Sidebar(Gtk.Revealer):
    def __init__(self, nodes=None, edges=None, widget=None):
        Gtk.Revealer.__init__(self)
        tabs = Gtk.Notebook.new()
        
        tab = Gtk.ScrolledWindow.new()
        tabs.append_page(tab, Gtk.Label(label="First tab"))
        self.add(tabs)
        self.set_reveal_child(True)
        self.show_all()
        
    def toggleVisibility(self):
        if self.get_child_revealed():
            self.set_reveal_child(False)
            self.set_visible(False)
        else:
            self.set_reveal_child(True)
            self.set_visible(True)


class AppWindow(Gtk.ApplicationWindow):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        box = Gtk.HBox()
        
        pane = Gtk.HPaned(wide_handle=True) # makes for a better separator
        box.add(pane)
        
        sidebar = Sidebar()
        pane.pack1(sidebar, False, True)

        # content widget, here just a simple "Hello World" label
        content = Gtk.Button(label="Hello World")
        content.connect("clicked", lambda x: sidebar.toggleVisibility())
        pane.pack2(content, True, False)

        self.add(box)
        self.set_size_request(200, 200)


class Application(Gtk.Application):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.window = None

    def do_activate(self):
        if not self.window:
            self.window = AppWindow(application=self, title="Main Window")
        self.window.show_all()
        self.window.present()


if __name__ == '__main__':
    app = Application()
    app.run()