[gnome-games] separate number-box out from gsudoku.py
- From: Thomas Hindoe Paaboel Andersen <thomashpa src gnome org>
- To: svn-commits-list gnome org
- Subject: [gnome-games] separate number-box out from gsudoku.py
- Date: Wed, 22 Apr 2009 05:14:33 -0400 (EDT)
commit 5f3577c43c1111d38b2d09de87ba37c060422783
Author: Zhang Sen <zh jesse gmail com>
Date: Wed Apr 22 08:16:00 2009 +0800
separate number-box out from gsudoku.py
---
gnome-sudoku/src/lib/gsudoku.py | 621 +-----------------------------------
gnome-sudoku/src/lib/number_box.py | 622 ++++++++++++++++++++++++++++++++++++
2 files changed, 626 insertions(+), 617 deletions(-)
diff --git a/gnome-sudoku/src/lib/gsudoku.py b/gnome-sudoku/src/lib/gsudoku.py
index 1140c9b..3b7e474 100644
--- a/gnome-sudoku/src/lib/gsudoku.py
+++ b/gnome-sudoku/src/lib/gsudoku.py
@@ -1,11 +1,11 @@
# -*- coding: utf-8 -*-
-import gtk, cairo, pango, gobject
+import gtk, gobject
import colors
import math
import random
from simple_debug import simple_debug
-from gettext import gettext as _
import sudoku
+import number_box
TRACKER_COLORS = [
# Use tango colors recommended here:
@@ -26,609 +26,11 @@ def gtkcolor_to_rgb (color):
color.green / float(2**16),
color.blue / float(2**16))
-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
-
-LITTLE_LINE_WIDTH = 0.25
-NORMAL_LINE_WIDTH = 1 # The size of the line we draw around a box
-
SPACING_FACTOR = 40 # The size of a box compared (roughly) to the size
# of padding -- the larger this is, the smaller
# the spaces
SMALL_TO_BIG_FACTOR = 3.5 # The number of times wider than a small line a big line is.
-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)
-
class SudokuNumberGrid (gtk.AspectFrame):
def __init__ (self, group_size = 9):
@@ -637,7 +39,7 @@ class SudokuNumberGrid (gtk.AspectFrame):
self.__entries__ = {}
for x in range(self.group_size):
for y in range(self.group_size):
- e = SudokuNumberBox(upper = self.group_size)
+ e = number_box.SudokuNumberBox(upper = self.group_size)
e.x = x
e.y = y
self.table.attach(e, x, x+1, y, y+1,
@@ -760,7 +162,6 @@ class SudokuGameDisplay (SudokuNumberGrid, gobject.GObject):
self.impossibilities = []
self.trackers = {}
self.__trackers_tracking__ = {}
- self.__colors_used__ = [None, ERROR_HIGHLIGHT_COLOR]
gobject.GObject.__init__(self)
SudokuNumberGrid.__init__(self, group_size = group_size)
self.setup_grid(grid, group_size)
@@ -937,7 +338,6 @@ class SudokuGameDisplay (SudokuNumberGrid, gobject.GObject):
self.impossible_hints = 0
self.trackers = {}
self.__trackers_tracking__ = {}
- self.__colors_used__ = [None, ERROR_HIGHLIGHT_COLOR]
self.blank_grid()
self.setup_grid(grid, group_size)
@@ -1286,21 +686,8 @@ if __name__ == '__main__':
window.add(sgd)
window.show_all()
- 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 ():
- nbox = NumberBox()
- window.add(nbox)
-
# test_number_grid()
# reproduce_foobared_rendering()
-# test_sudoku_game()
-# test_number_selector()
- test_number_box()
+ test_sudoku_game()
window.show_all()
gtk.main()
diff --git a/gnome-sudoku/src/lib/number_box.py b/gnome-sudoku/src/lib/number_box.py
new file mode 100644
index 0000000..434dd74
--- /dev/null
+++ b/gnome-sudoku/src/lib/number_box.py
@@ -0,0 +1,622 @@
+# -*- 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()
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]