Skip to content

Instantly share code, notes, and snippets.

@sahib
Created June 8, 2013 10:37
Show Gist options
  • Save sahib/5734790 to your computer and use it in GitHub Desktop.
Save sahib/5734790 to your computer and use it in GitHub Desktop.
Working version of a GtkStatusIcon drawn with Cairo.
#!/usr/bin/env python
# encoding: utf-8
from gi.repository import Gtk, Gdk, GLib, Pango, PangoCairo
from cairo import Context, ImageSurface, RadialGradient, FORMAT_ARGB32
from math import pi
def draw_center_text(ctx, width, height, text, font_size=15):
layout = PangoCairo.create_layout(ctx)
font = Pango.FontDescription()
font.set_size(font_size * Pango.SCALE)
layout.set_font_description(font)
layout.set_text(text, -1)
fw, fh = [num / Pango.SCALE / 2 for num in layout.get_size()]
ctx.move_to(width / 2 - fw, height / 2 - fh)
PangoCairo.show_layout(ctx, layout)
def draw_stopped(ctx, width, height, mx, my):
rx, ry = width / 7, height / 7
# Simply paint a square rectangle
ctx.rectangle(mx - rx, mx - ry, 2 * rx, 2 * ry)
ctx.fill()
def draw_paused(ctx, width, height, mx, my):
rx, ry = width / 5, height / 5
lx = rx * 2 / 2.5
# Paint two vertical Bars
ctx.rectangle(mx - rx, my - ry, lx, 2 * ry)
ctx.rectangle(mx + lx / 2, my - ry, lx, 2 * ry)
ctx.fill()
def draw_playing(ctx, width, height, mx, my):
rx, ry = width / 5, height / 5
# move the triangle to the centroid
# this improves appearance
mx += rx / 3
# Paint a Triangle
ctx.move_to(mx - rx, my - ry)
ctx.line_to(mx + rx, my)
ctx.line_to(mx - rx, my + ry)
ctx.line_to(mx - rx, my - ry)
ctx.fill()
def draw_undefined(ctx, width, height, mx, my):
# Draw a nice unicode symbol in the middle
draw_center_text(ctx, width, height, '❂', font_size=0.5 * width)
DRAW_STATE_MAP = {
'playing': draw_playing,
'stopped': draw_stopped,
'paused': draw_paused,
'undefined': draw_undefined
}
def draw_status_icon(state, col_insens, col_active, col_middle, percent=0, width=70, height=70):
# Create a new Surface (we get a Gdk.Pixbuf from it later)
surface = ImageSurface(FORMAT_ARGB32, width, height)
# For actual drawing we need a Context
ctx = Context(surface)
# Middle points
mx, my = width / 2, height / 2
# Border Width
bw = min(width, height) / 8
ctx.set_line_width(bw)
# Number of segments and length in radiants
segments = 12
base = (pi * 2) / segments
left90 = (pi / 2)
radius = min(mx, my) - (bw / 2 + 1)
# Draw a black circle in the middle of the segments (non-touching)
rg = RadialGradient(mx, my, radius / 2, mx, my, min(mx, my))
rg.add_color_stop_rgb(0, 0.1, 0.1, 0.1)
rg.add_color_stop_rgb(1, 0.4, 0.4, 0.4)
ctx.set_source(rg)
ctx.arc(mx, my, min(mx, my) - 1, 0, 2 * pi)
ctx.fill()
# Draw {segments} curvy segments
for segment in range(segments):
# Decide which color the segment should have
if segment / segments * 100 >= percent:
ctx.set_source_rgb(*col_insens)
else:
ctx.set_source_rgb(*col_active)
# Draw a segment
ctx.arc(
mx, my, radius,
segment * base - left90,
(0.75 + segment) * base - left90
)
ctx.stroke()
# Now draw an informative symbol into the middle of the black circle
ctx.set_source_rgb(*col_middle)
DRAW_STATE_MAP[state](ctx, width, height, mx, my)
# Convert the surface to an actual Gdk.Pixbuf we can set to the StatusIcon
return Gdk.pixbuf_get_from_surface(
surface, 0, 0,
surface.get_width(),
surface.get_height()
)
# TODO: Inherit from Gtk.StatusIcon
class TrayIcon(object):
def __init__(self):
self._icon = Gtk.StatusIcon()
self._icon.set_visible(True)
self._icon.connect('popup-menu', self.on_showpopup)
# Colors - fixed, not from theme
self._col_active = (0.5, 0.7, 0.5)
self._col_insens = (0.4, 0.4, 0.4)
self._col_middle = (1.0, 1.0, 1.0)
# Settings:
self._percent = 0
self._state = 'undefined'
self._redraw()
# Popups:
self._menu_items = []
def _redraw(self):
pixbuf = draw_status_icon(
self._state,
col_active=self._col_active,
col_insens=self._col_insens,
col_middle=self._col_middle,
percent=self._percent
)
self._icon.set_from_pixbuf(pixbuf)
def create_menu_item(self, text, stock_id=None, signal=None):
menu_item = Gtk.ImageMenuItem(text)
if signal is not None:
menu_item.connect('button-press-event', signal)
if stock_id is not None:
image = Gtk.Image()
image.set_from_stock(stock_id, Gtk.IconSize.MENU)
menu_item.set_image(image)
#
self._menu_items.append(menu_item)
def create_separator_item(self):
self._menu_items.append(Gtk.SeparatorMenuItem())
def get_menu_items(self):
return self._menu_items
#############
# Signals #
#############
def on_showpopup(self, icon, button, time):
menu = Gtk.Menu()
for menu_item in self._menu_items:
menu.append(menu_item)
menu.show_all()
menu.popup(
None, None,
lambda w, x: self._icon.position_menu(menu, self._icon),
self._icon, 3, time
)
################
# Properties #
################
def get_percent(self):
return self._percent
def set_percent(self, val):
self._percent = val
self._redraw()
percent = property(get_percent, set_percent)
def get_state(self):
return self._state
def set_state(self, val):
keys = DRAW_STATE_MAP.keys()
if val in keys:
self._state = val
self._redraw()
else:
raise ValueError('Must be one of ' + str(keys))
state = property(get_state, set_state)
states = list(DRAW_STATE_MAP.keys())
if __name__ == '__main__':
ti = TrayIcon()
def increment_percent():
global states
ti.percent += 10
if ti.percent > 100:
ti.percent = 0
try:
ti.state = states.pop()
except IndexError:
states = list(DRAW_STATE_MAP.keys())
return True
ti.create_menu_item(
'Stop',
stock_id='gtk-media-stop',
signal=lambda *a: Gtk.main_quit()
)
ti.create_menu_item(
'Pause',
stock_id='gtk-media-pause',
signal=lambda *a: Gtk.main_quit()
)
ti.create_menu_item(
'Previous',
stock_id='gtk-media-previous',
signal=lambda *a: Gtk.main_quit()
)
ti.create_menu_item(
'Next',
stock_id='gtk-media-next',
signal=lambda *a: Gtk.main_quit()
)
ti.create_separator_item()
ti.create_menu_item(
'Quit',
stock_id='gtk-quit',
signal=lambda *a: Gtk.main_quit()
)
GLib.timeout_add(100, increment_percent)
Gtk.main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment