Viewing file: number_box.py (20.88 KB) -rw-r--r-- Select action/file-type: (+) | (+) | (+) | Code (+) | Session (+) | (+) | SDB (+) | (+) | (+) | (+) | (+) | (+) |
# -*- coding: utf-8 -*- #!/usr/bin/python
import gtk, gobject, pango, cairo import math from gettext import gettext as _
ERROR_HIGHLIGHT_COLOR = (1.0, 0, 0)
BASE_SIZE = 35 # The "normal" size of a box (in pixels)
# And the standard font-sizes -- these should fit nicely with the # BASE_SIZE BASE_FONT_SIZE = pango.SCALE * 13 NOTE_FONT_SIZE = pango.SCALE * 6
BORDER_WIDTH = 9.0 # The size of space we leave for a box BORDER_LINE_WIDTH = 4 # The size of the line we draw around a selected box NORMAL_LINE_WIDTH = 1 # The size of the line we draw around a box
class NumberSelector (gtk.EventBox):
__gsignals__ = { 'changed':(gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), }
def __init__ (self, default = None, upper = 9): self.value = default gtk.EventBox.__init__(self) self.table = gtk.Table() self.add(self.table) side = int(math.sqrt(upper)) n = 1 for y in range(side): for x in range(side): b = gtk.Button() l = gtk.Label() if n == self.value: l.set_markup('<b><span size="x-small">%s</span></b>'%n) else: l.set_markup('<span size="x-small">%s</span>'%n) b.add(l) b.set_relief(gtk.RELIEF_HALF) l = b.get_children()[0] b.set_border_width(0) l.set_padding(0, 0) l.get_alignment() b.connect('clicked', self.number_clicked, n) self.table.attach(b, x, x+1, y, y+1) n += 1 if self.value: db = gtk.Button() l = gtk.Label() l.set_markup_with_mnemonic('<span size="x-small">'+_('_Clear')+'</span>') db.add(l) l.show() db.connect('clicked', self.number_clicked, 0) self.table.attach(db, 0, side, side + 1, side + 2) self.show_all()
def number_clicked (self, button, n): self.value = n self.emit('changed')
def get_value (self): return self.value
def set_value (self, n): self.value = n
class NumberBox (gtk.Widget):
text = '' top_note_text = '' bottom_note_text = '' read_only = False _layout = None _top_note_layout = None _bottom_note_layout = None text_color = None highlight_color = None custom_background_color = None
__gsignals__ = { 'value-about-to-change':(gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), 'changed':(gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), # undo-change - A hacky way to handle the fact that we want to # respond to undo's changes but we don't want undo to respond # to itself... 'undo-change':(gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), 'notes-changed':(gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), }
base_state = gtk.STATE_NORMAL npicker = None draw_boxes = False
def __init__ (self, upper = 9, text = ''): gtk.Widget.__init__(self) self.upper = upper self.font = self.style.font_desc self.font.set_size(BASE_FONT_SIZE) self.note_font = self.font.copy() self.note_font.set_size(NOTE_FONT_SIZE) self.set_property('can-focus', True) self.set_property('events', gtk.gdk.ALL_EVENTS_MASK) self.connect('button-press-event', self.button_press_cb) self.connect('key-release-event', self.key_press_cb) self.connect('enter-notify-event', self.pointer_enter_cb) self.connect('leave-notify-event', self.pointer_leave_cb) self.connect('focus-in-event', self.focus_in_cb) self.connect('focus-out-event', self.focus_out_cb) self.connect('motion-notify-event', self.motion_notify_cb) self.set_text(text)
def pointer_enter_cb (self, *args): if not self.is_focus(): self.set_state(gtk.STATE_PRELIGHT)
def pointer_leave_cb (self, *args): self.set_state(self.base_state) self._toggle_box_drawing_(False)
def focus_in_cb (self, *args): self.set_state(gtk.STATE_SELECTED) self.base_state = gtk.STATE_SELECTED
def focus_out_cb (self, *args): self.set_state(gtk.STATE_NORMAL) self.base_state = gtk.STATE_NORMAL self.destroy_npicker()
def destroy_npicker (self): if self.npicker: self.npicker.destroy() self.npicker = None
def motion_notify_cb (self, *args): if self.is_focus() and not self.read_only: self._toggle_box_drawing_(True) else: self._toggle_box_drawing_(False)
def _toggle_box_drawing_ (self, val): if val and not self.draw_boxes: self.draw_boxes = True self.queue_draw() if (not val) and self.draw_boxes: self.draw_boxes = False self.queue_draw()
def button_press_cb (self, w, e): if self.read_only: return if e.type == gtk.gdk._2BUTTON_PRESS: # ignore second click (this makes a double click in the # middle of a cell get us a display of the numbers, rather # than selecting a number. return if self.is_focus(): x, y = e.get_coords() alloc = self.get_allocation() my_w = alloc.width my_h = alloc.height border_height = float(BORDER_WIDTH)/BASE_SIZE
if float(y)/my_h < border_height: self.show_note_editor(top = True) elif float(y)/my_h > (1-border_height): self.show_note_editor(top = False) elif not self.npicker: # In this case we're a normal old click... # makes sure there is only one numer selector self.show_number_picker() else: self.grab_focus()
def key_press_cb (self, w, e): if self.read_only: return if self.npicker: # kill number picker no matter what is pressed self.destroy_npicker() txt = gtk.gdk.keyval_name(e.keyval) if type(txt) == type(None): # Make sure we don't trigger on unplugging the A/C charger etc return txt = txt.replace('KP_', '') if self.get_text() == txt: # If there's no change, do nothing return if txt in ['0', 'Delete', 'BackSpace']: self.set_text_interactive('') elif txt in ['n', 'N']: self.show_note_editor(top = True) elif txt in ['m', 'M']: self.show_note_editor(top = False) # And then add the new value if need be elif txt in [str(n) for n in range(1, self.upper+1)]: # First do a removal event -- this is something of a # kludge, but it works nicely with old code that was based # on entries, which also behave this way (they generate 2 # events for replacing a number with a new number - a # removal event and an addition event) if self.get_text(): self.set_text_interactive('') # Then add self.set_text_interactive(txt)
def note_changed_cb (self, w, top = False): if top: self.set_note_text_interactive(top_text = w.get_text()) else: self.set_note_text_interactive(bottom_text = w.get_text())
def show_note_editor (self, top = True): alloc = self.get_allocation() w = gtk.Window() w.set_property('skip-pager-hint', True) w.set_property('skip-taskbar-hint', True) w.set_decorated(False) w.set_position(gtk.WIN_POS_MOUSE) w.set_size_request(alloc.width, alloc.height/2) f = gtk.Frame() e = gtk.Entry() f.add(e) if top: e.set_text(self.top_note_text) else: e.set_text(self.bottom_note_text) w.add(f) e.connect('changed', self.note_changed_cb, top) e.connect('focus-out-event', lambda e, ev, w: w.destroy(), w) e.connect('activate', lambda e, w: w.destroy(), w) x, y = self.window.get_origin() if top: w.move(x, y) else: w.move(x, y+int(alloc.height*0.6)) w.show_all() e.grab_focus()
def number_changed_cb (self, num_selector): self.destroy_npicker() self.set_text_interactive('') newval = num_selector.get_value() if newval: self.set_text_interactive(str(newval))
def show_number_picker (self): w = gtk.Window(type = gtk.WINDOW_POPUP) ns = NumberSelector(upper = self.upper, default = self.get_value()) ns.connect('changed', self.number_changed_cb) w.grab_focus() w.add(ns) r = w.get_allocation() my_origin = self.window.get_origin() x, y = self.window.get_size() popupx, popupy = w.get_size() overlapx = popupx-x overlapy = popupy-y w.move(my_origin[0]-(overlapx/2), my_origin[1]-(overlapy/2)) w.show() self.npicker = w
def set_text_interactive (self, text): self.emit('value-about-to-change') self.set_text(text) self.queue_draw() self.emit('changed')
def set_font (self, font): if type(font) == str: font = pango.FontDescription(font) self.font = font if self.text: self.set_text(self.text) self.queue_draw()
def set_note_font (self, font): if type(font) == str: font = pango.FontDescription(font) self.note_font = font if self.top_note_text or self.bottom_note_text: self.set_note_text(self.top_note_text, self.bottom_note_text) self.queue_draw()
def set_text (self, text): self.text = text self._layout = self.create_pango_layout(text) self._layout.set_font_description(self.font)
def set_notes (self, notes): """Hackish method to allow easy use of Undo API.
Undo API requires a set method that is called with one argument (the result of a get method)""" self.set_note_text(top_text = notes[0], bottom_text = notes[1]) self.queue_draw()
def set_note_text (self, top_text = None, bottom_text = None): if top_text is not None: self.top_note_text = top_text self._top_note_layout = self.create_pango_layout(top_text) self._top_note_layout.set_font_description(self.note_font) if bottom_text is not None: self.bottom_note_text = bottom_text self._bottom_note_layout = self.create_pango_layout(bottom_text) self._bottom_note_layout.set_font_description(self.note_font) self.queue_draw()
def set_note_text_interactive (self, *args, **kwargs): self.emit('value-about-to-change') self.set_note_text(*args, **kwargs) self.emit('notes-changed')
def do_realize (self): # The do_realize method is responsible for creating GDK (windowing system) # resources. In this example we will create a new gdk.Window which we # then draw on
# First set an internal flag telling that we're realized self.set_flags(self.flags() | gtk.REALIZED)
# Create a new gdk.Window which we can draw on. # Also say that we want to receive exposure events by setting # the event_mask self.window = gtk.gdk.Window( self.get_parent_window(), width = self.allocation.width, height = self.allocation.height, window_type = gtk.gdk.WINDOW_CHILD, wclass = gtk.gdk.INPUT_OUTPUT, event_mask = self.get_events() | gtk.gdk.EXPOSURE_MASK)
# Associate the gdk.Window with ourselves, Gtk+ needs a reference # between the widget and the gdk window self.window.set_user_data(self)
# Attach the style to the gdk.Window, a style contains colors and # GC contextes used for drawing self.style.attach(self.window)
# The default color of the background should be what # the style (theme engine) tells us. self.style.set_background(self.window, gtk.STATE_NORMAL) self.window.move_resize(*self.allocation)
def do_unrealize (self): # The do_unrealized method is responsible for freeing the GDK resources
# De-associate the window we created in do_realize with ourselves self.window.set_user_data(None)
def do_size_request (self, requisition): # The do_size_request method Gtk+ is calling on a widget to ask # it the widget how large it wishes to be. It's not guaranteed # that gtk+ will actually give this size to the widget
# In this case, we say that we want to be as big as the # text is, and a square width, height = self._layout.get_size() if width > height: side = width/pango.SCALE else: side = height/pango.SCALE (requisition.width, requisition.height) = (side, side)
def do_size_allocate(self, allocation): # The do_size_allocate is called by when the actual size is known # and the widget is told how much space could actually be allocated
# Save the allocated space self.allocation = allocation
# If we're realized, move and resize the window to the # requested coordinates/positions if self.flags() & gtk.REALIZED: self.window.move_resize(*allocation)
def do_expose_event(self, event): # The do_expose_event is called when the widget is asked to draw itself # Remember that this will be called a lot of times, so it's usually # a good idea to write this code as optimized as it can be, don't # Create any resources in here. x, y, w, h = self.allocation cr = self.window.cairo_create() if h < w: scale = h/float(BASE_SIZE) else: scale = w/float(BASE_SIZE) cr.scale(scale, scale) self.draw_background_color(cr) if self.is_focus(): self.draw_highlight_box(cr) self.draw_normal_box(cr) self.draw_text(cr) if self.draw_boxes and self.is_focus(): self.draw_note_area_highlight_box(cr)
def draw_background_color (self, cr): if self.read_only: if self.custom_background_color: r, g, b = self.custom_background_color cr.set_source_rgb( r*0.6, g*0.6, b*0.6 ) else: cr.set_source_color(self.style.base[gtk.STATE_INSENSITIVE]) elif self.is_focus(): cr.set_source_color(self.style.base[gtk.STATE_SELECTED]) elif self.custom_background_color: cr.set_source_rgb(*self.custom_background_color) else: cr.set_source_color( self.style.base[self.state] ) cr.rectangle( 0, 0, BASE_SIZE, BASE_SIZE ) cr.fill()
def draw_normal_box (self, cr): state = self.state if state == gtk.STATE_SELECTED: # When the widget is selected, we still want the outer box to look normal state = gtk.STATE_NORMAL cr.set_source_color( self.style.mid[state] ) cr.rectangle( NORMAL_LINE_WIDTH*0.5, NORMAL_LINE_WIDTH*0.5, BASE_SIZE-NORMAL_LINE_WIDTH, BASE_SIZE-NORMAL_LINE_WIDTH, ) cr.set_line_width(NORMAL_LINE_WIDTH) cr.set_line_join(cairo.LINE_JOIN_ROUND) cr.stroke() # And now draw a thinner line around the very outside... cr.set_source_color( self.style.dark[state] ) cr.rectangle( NORMAL_LINE_WIDTH*0.25, NORMAL_LINE_WIDTH*0.25, BASE_SIZE-NORMAL_LINE_WIDTH*0.5, BASE_SIZE-NORMAL_LINE_WIDTH*0.5, ) cr.set_line_width(NORMAL_LINE_WIDTH*0.5) cr.set_line_join(cairo.LINE_JOIN_MITER) cr.stroke()
def draw_highlight_box (self, cr): cr.set_source_color( self.style.base[gtk.STATE_SELECTED] ) cr.rectangle( # left-top BORDER_LINE_WIDTH*0.5, BORDER_LINE_WIDTH*0.5, # bottom-right BASE_SIZE-(BORDER_LINE_WIDTH), BASE_SIZE-(BORDER_LINE_WIDTH), ) cr.set_line_width(BORDER_LINE_WIDTH) cr.set_line_join(cairo.LINE_JOIN_ROUND) cr.stroke()
def draw_note_area_highlight_box (self, cr): # set up our paint brush... cr.set_source_color( self.style.mid[self.state] ) cr.set_line_width(NORMAL_LINE_WIDTH) cr.set_line_join(cairo.LINE_JOIN_ROUND) # top rectangle cr.rectangle(NORMAL_LINE_WIDTH*0.5, NORMAL_LINE_WIDTH*0.5, BASE_SIZE-NORMAL_LINE_WIDTH, BORDER_WIDTH-NORMAL_LINE_WIDTH) cr.stroke() # bottom rectangle cr.rectangle(NORMAL_LINE_WIDTH*0.5, #x BASE_SIZE - BORDER_WIDTH-(NORMAL_LINE_WIDTH*0.5), #y BASE_SIZE-NORMAL_LINE_WIDTH, #x2 BASE_SIZE-NORMAL_LINE_WIDTH #y2 ) cr.stroke()
def draw_text (self, cr): if self.text_color: cr.set_source_rgb(*self.text_color) elif self.read_only: cr.set_source_color(self.style.text[gtk.STATE_NORMAL]) else: cr.set_source_color(self.style.text[self.state]) # And draw the text in the middle of the allocated space if self._layout: fontw, fonth = self._layout.get_pixel_size() cr.move_to( (BASE_SIZE/2)-(fontw/2), (BASE_SIZE/2) - (fonth/2), ) cr.update_layout(self._layout) cr.show_layout(self._layout) cr.set_source_color(self.style.text[self.state]) # And draw any note text... if self._top_note_layout: fontw, fonth = self._top_note_layout.get_pixel_size() cr.move_to( NORMAL_LINE_WIDTH, 0, ) cr.update_layout(self._top_note_layout) cr.show_layout(self._top_note_layout) if self._bottom_note_layout: fontw, fonth = self._bottom_note_layout.get_pixel_size() cr.move_to( NORMAL_LINE_WIDTH, BASE_SIZE-fonth, ) cr.update_layout(self._bottom_note_layout) cr.show_layout(self._bottom_note_layout)
def set_text_color (self, color): self.text_color = color self.queue_draw()
def set_background_color (self, color): self.custom_background_color = color self.queue_draw()
def hide_notes (self): pass
def show_notes (self): pass
def set_value_from_undo (self, v): self.set_value(v) self.emit('undo_change')
def set_value (self, v): if 0 < v <= self.upper: self.set_text(str(v)) else: self.set_text('') self.queue_draw()
def get_value (self): try: return int(self.text) except: return None
def get_text (self): return self.text
def get_note_text (self): return self.top_note_text, self.bottom_note_text
class SudokuNumberBox (NumberBox):
normal_color = None highlight_color = ERROR_HIGHLIGHT_COLOR
def set_color (self, color): self.normal_color = color self.set_text_color(self.normal_color)
def unset_color (self): self.set_color(None)
def set_error_highlight (self, val): if val: self.set_text_color((1.0, 0, 0)) else: self.set_text_color(self.normal_color)
def set_read_only (self, val): self.read_only = val if not hasattr(self, 'bold_font'): self.normal_font = self.font self.bold_font = self.font.copy() self.bold_font.set_weight(pango.WEIGHT_BOLD) if self.read_only: self.set_font(self.bold_font) else: self.set_font(self.normal_font) self.queue_draw()
def set_impossible (self, val): if val: self.set_text('X') else: self.set_text('')
gobject.type_register(NumberBox)
if __name__ == '__main__': window = gtk.Window() window.connect('delete-event', gtk.main_quit)
def test_number_selector (): nselector = NumberSelector(default = 3) def tell_me (b): print 'value->', b.get_value() nselector.connect('changed', tell_me) window.add(nselector)
def test_number_box (): window.set_size_request(100, 100) nbox = NumberBox() window.add(nbox)
# test_number_selector() test_number_box() window.show_all() gtk.main()
|