Created
June 8, 2013 10:37
-
-
Save sahib/5734790 to your computer and use it in GitHub Desktop.
Working version of a GtkStatusIcon drawn with Cairo.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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