Skip to content

Instantly share code, notes, and snippets.

@cvgarciarea
Last active July 18, 2024 08:14
Show Gist options
  • Select an option

  • Save cvgarciarea/21cbccc3fd00749e40f22a294d89767f to your computer and use it in GitHub Desktop.

Select an option

Save cvgarciarea/21cbccc3fd00749e40f22a294d89767f to your computer and use it in GitHub Desktop.
A Gtk+ Window with a tabs on HeaderBar.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import gi
gi.require_versions({"Gtk": "3.0", "Gdk": "3.0"})
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GObject
ICONSIZE = Gtk.IconSize.MENU
get_icon = lambda name: Gtk.Image.new_from_icon_name(name, ICONSIZE)
class Tab(Gtk.HBox):
__gsignals__ = {
"close": (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, []),
}
def __init__(self):
Gtk.HBox.__init__(self)
self.set_hexpand(False)
self.set_vexpand(False)
self._close_btn = Gtk.Button()
self._close_btn.get_style_context().add_class("titlebutton")
self._close_btn.get_style_context().add_class("circular")
self._close_btn.set_margin_left(3)
self._close_btn.add(get_icon("window-close"))
self._close_btn.connect("clicked", self._close_cb)
self.pack_end(self._close_btn, False, False, 0)
def _close_cb(self, button):
self.emit("close")
class HeaderTabWindow(Gtk.Window):
__gsignals__ = {
"new-tab": (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, []),
"close-tab": (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, [GObject.TYPE_PYOBJECT]),
}
def __init__(self):
Gtk.Window.__init__(self)
self._free_space = 100
self._maximized = False
self._children = {}
self._count = 0
self._bar = Gtk.HeaderBar()
self._bar.set_show_close_button(False)
self._bar.connect("size-allocate", self._size_allocate_cb)
self.set_titlebar(self._bar)
self._button_box = Gtk.HBox()
self._button_box.get_style_context().add_class("right")
self._bar.pack_end(self._button_box)
separator = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)
separator.get_style_context().add_class("titlebutton")
self._button_box.pack_start(separator, False, False, 3)
self._minimize_btn = Gtk.Button()
self._minimize_btn.get_style_context().add_class("titlebutton")
self._minimize_btn.get_style_context().add_class("minimize")
self._minimize_btn.add(get_icon("window-minimize-symbolic"))
self._minimize_btn.connect("clicked", self._minimize_cb)
self._button_box.pack_start(self._minimize_btn, False, False, 3)
self._maximize_btn = Gtk.Button()
self._maximize_btn.get_style_context().add_class('titlebutton')
self._maximize_btn.get_style_context().add_class('maximize')
self._maximize_btn.add(get_icon('window-maximize-symbolic'))
self._maximize_btn.connect('clicked', self._maximize_cb)
self._button_box.pack_start(self._maximize_btn, False, False, 3)
self._close_btn = Gtk.Button()
self._close_btn.get_style_context().add_class("titlebutton")
self._close_btn.get_style_context().add_class("close")
self._close_btn.add(get_icon("window-close-symbolic"))
self._close_btn.connect("clicked", self._close_cb)
self._button_box.pack_start(self._close_btn, False, False, 3)
self._notebook = Gtk.Notebook()
self._notebook.set_scrollable(True)
self._notebook.set_valign(Gtk.Align.END)
self._notebook.set_show_border(False)
self._notebook.set_size_request(20, 1)
self._notebook.connect("switch-page", self._switch_page_cb)
self._notebook.connect("page-removed", self._page_removed_cb)
self._bar.pack_start(self._notebook)
self._hbox = Gtk.HBox()
self._notebook.set_action_widget(self._hbox, Gtk.PackType.END)
self._newtab_btn = Gtk.Button()
self._newtab_btn.add(get_icon("tab-new"))
self._newtab_btn.get_style_context().add_class("titlebutton")
self._newtab_btn.connect("clicked", self._new_tab_cb)
self._hbox.pack_start(self._newtab_btn, False, False, 0)
self._hbox.show_all()
self._canvas = Gtk.VBox()
self.add(self._canvas)
def _size_allocate_cb(self, window, rect):
required = self._free_space + self._button_box.get_allocation().width + self._newtab_btn.get_allocation().width
if rect.width < required:
return
children = self._notebook.get_children()
newtab_alloc = self._newtab_btn.get_allocation()
min_width = newtab_alloc.width
if children:
cwidget = self._notebook.get_nth_page(self._notebook.get_current_page())
min_width += cwidget.get_allocation().width
hrect = self._bar.get_allocation()
nrect = Gdk.Rectangle()
nrect.x = hrect.x
nrect.y = hrect.y
nrect.width = max(rect.width - required, min_width)
nrect.height = self._bar.get_allocation().height
Gtk.Widget.size_allocate(self._notebook, nrect)
showing_arrows = False
if children:
last_tab = self._notebook.get_tab_label(self._notebook.get_children()[-1])
lrect = self._notebook.get_tab_label(self._notebook.get_children()[-1]).get_allocation()
tabs_width = 0
for child in children:
tab = self._notebook.get_tab_label(child)
tabs_width += tab.get_allocation().width
showing_arrows = (tabs_width >= self._notebook.get_allocation().width)
_x = lrect.x + lrect.width
if _x <= 0 or _x > nrect.width - newtab_alloc.width:
_x = nrect.width - newtab_alloc.width
else:
_x += 10
else:
_x = hrect.x
if showing_arrows:
_x += 5
hrect = Gdk.Rectangle()
hrect.y = nrect.y + nrect.height - newtab_alloc.height
hrect.x = _x
hrect.width = nrect.width
hrect.height = newtab_alloc.height
Gtk.Widget.size_allocate(self._hbox, hrect)
def _close_cb(self, widget):
self.close()
def _minimize_cb(self, widget):
self.iconify()
def _maximize_cb(self, widget):
self.unmaximize() if self._maximized else self.maximize()
def _window_state_cb(self, widget, event):
self._maximized = bool(event.new_window_state & Gdk.WindowState.MAXIMIZED)
def _switch_page_cb(self, notebook, widget, num):
if self._canvas.get_children():
self._canvas.remove(self._canvas.get_children()[0])
walloc = self._notebook.get_tab_label(widget).get_allocation()
balloc = self._newtab_btn.get_allocation()
self._notebook.set_size_request(walloc.width + balloc.width, 1)
self._canvas.pack_start(self._children[widget.id].child, True, True, 0)
self._children[widget.id].child.show()
def _page_removed_cb(self, notebook, widget, num):
if num == self._notebook.get_current_page():
self._canvas.remove(self._children[widget.id].child)
elif self._notebook.get_children() == [] and self._canvas.get_children() != []:
self._canvas.remove(self._canvas.get_children()[0])
self._children.pop(widget.id)
def _new_tab_cb(self, button):
self.emit("new-tab")
def append_page(self, tab):
box = Gtk.Box()
box.id = self._count
self._children[box.id] = tab
self._count += 1
tab.connect("close", lambda t: self.emit("close-tab", tab))
self._notebook.append_page(box, tab)
self._notebook.set_tab_reorderable(box, True)
tab.show_all()
self.show_all()
def get_nth_page(self, widget):
return self._notebook.get_nth_page(widget)
def page_num(self, widget):
return self._notebook.page_num(widget)
def remove_page(self, idx):
self._notebook.remove_page(idx)
if __name__ == "__main__":
import signal, sys
signal.signal(signal.SIGINT, signal.SIG_DFL)
def _re(win, *args):
tab = Tab()
_id = str(len(win._notebook.get_children()))
tab.pack_start(Gtk.Label("Tab " + _id), False, False, 0)
tab.child = Gtk.Label("Child test " + _id)
win.append_page(tab)
tab.show_all()
def _cl(win, tab):
win.remove_page(win.page_num(tab.child))
win = HeaderTabWindow()
win.connect("destroy", Gtk.main_quit)
win.connect("realize", _re)
win.connect("new-tab", _re)
win.connect("close-tab", _cl)
win.show_all()
win._notebook.show_all()
Gtk.main()
sys.exit(1)
@cvgarciarea
Copy link
Author

Don't judge me! All with Adwaita looks horrible :P
Adwaita theme

OSX Arc Darker theme

@nykula
Copy link

nykula commented Nov 6, 2018

Hello. Application priority styles can make it look nicer with GTK defaults. Here's what I have in a JS app.

const css = new CssProvider();
css.load_from_data(`
  headerbar { min-height: 0; padding: 0; }
  headerbar button { margin: 0; }
  notebook box button { color: inherit; }
  notebook box label { padding-top: 0.275em; }
`);
StyleContext.add_provider_for_screen(
  win.get_screen(),
  css,
  STYLE_PROVIDER_PRIORITY_APPLICATION
);

That, and ReliefStyle.NONE for buttons with symbolic icons.

screen shot 2018-11-06 at 22 16 35

But how do you let the user reorder tabs? I'm trying your example with Gtk 3.22, and like in my app, dragging moves the whole window, as if there wasn't any notebook. Only when I place the notebook outside of the window titlebar, reorderable starts working as I expect and emits my page-reordered signal callback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment