# -*- coding: utf-8 -*- # This code is from GNOME Sudoku # Adapted by: Daniel Francis from gi.repository import Gtk,Gdk,GObject,Pango,PangoCairo import math import random import tracker_info from gettext import gettext as _ # simple_debug import logging logger = logging.getLogger('number_box') simple_debug = logger.debug 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 NORMAL_LINE_WIDTH = 1 # The size of the line we draw around a box DEBUG_COLORS = False def debug_set_color_rgba(cr, rgba): COLORS = ("red","green","blue","yellow","purple","wheat","maroon","gray") if DEBUG_COLORS: rgba = Gdk.RGBA() rgba.parse(COLORS[random.randint(0,len(COLORS)-1)]) Gdk.cairo_set_source_rgba(cr, rgba) class NumberSelector (Gtk.EventBox): __gsignals__ = { 'changed':(GObject.SignalFlags.RUN_LAST, None, ()), } def __init__ (self, default = None, upper = 9): self.value = default GObject.GObject.__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('%s'%n) else: l.set_markup('%s'%n) b.add(l) b.set_relief(Gtk.ReliefStyle.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(''+_('_Clear')+'') 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.DrawingArea): 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 shadow_color = None custom_background_color = None border_color = None __gsignals__ = { 'value-about-to-change':(GObject.SignalFlags.RUN_LAST, None, ()), 'notes-about-to-change':(GObject.SignalFlags.RUN_LAST, None, ()), 'changed':(GObject.SignalFlags.RUN_LAST, 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.SignalFlags.RUN_LAST, None, ()), 'notes-changed':(GObject.SignalFlags.RUN_LAST, None, ()), } base_state = Gtk.StateFlags.NORMAL npicker = None draw_boxes = False def __init__ (self, upper = 9, text = ''): Gtk.DrawingArea.__init__(self) self.upper = upper self.parent_win = None self.timer = None self.font = self.get_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._top_note_layout = Pango.Layout(self.create_pango_context()) self._top_note_layout.set_font_description(self.note_font) self._bottom_note_layout = Pango.Layout(self.create_pango_context()) self._bottom_note_layout.set_font_description(self.note_font) self._base_stateflags = Gtk.StateFlags.NORMAL self.top_note_list = [] self.bottom_note_list = [] self.tinfo = tracker_info.TrackerInfo() self.set_property('can-focus', True) self.set_property('events', Gdk.EventMask.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 set_parent_win(self, new_parent): self.parent_win = new_parent def set_timer(self, new_timer): self.timer = new_timer def pointer_enter_cb (self, *args): if not self.is_focus(): self.set_state_flags(Gtk.StateFlags.PRELIGHT, False) def pointer_leave_cb (self, *args): self.set_state_flags(self._base_stateflags, True) self._toggle_box_drawing_(False) def focus_in_cb (self, *args): self.set_state_flags(Gtk.StateFlags.SELECTED, True) self._base_stateflags = Gtk.StateFlags.SELECTED def focus_out_cb (self, *args): self.set_state_flags(Gtk.StateFlags.NORMAL, True) self._base_stateflags = Gtk.StateFlags.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 == Gdk.EventType._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 = 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_', '') # Add the new value if need be if txt in [str(n) for n in range(1, self.upper+1)]: if e.state & Gdk.ModifierType.CONTROL_MASK: self.add_note_text(txt, top = True) elif e.state & Gdk.ModifierType.MOD1_MASK: self.remove_note_text(txt, top = True) elif self.get_text() != txt or \ (self.tracker_id != tracker_info.NO_TRACKER and self.tinfo.current_tracker == tracker_info.NO_TRACKER): # If there's no change, do nothing unless the player wants to # change a tracked item while not tracking(ie commit a tracked # change) self.set_text_interactive(txt) elif txt in ['0', 'Delete', 'BackSpace']: self.set_text_interactive('') elif txt in ['n', 'N']: if e.state & Gdk.ModifierType.MOD1_MASK: self.set_note_text_interactive(top_text = '') else: self.show_note_editor(top = True) elif txt in ['m', 'M']: if e.state & Gdk.ModifierType.MOD1_MASK: self.set_note_text_interactive(bottom_text = '') else: self.show_note_editor(top = False) def add_note_text(self, txt, top = False): if top: note = self.top_note_text else: note = self.bottom_note_text if txt not in note: tmp = list(note) tmp.append(txt) tmp.sort() note = ''.join(tmp) if top: self.set_note_text_interactive(top_text = note) else: self.set_note_text_interactive(bottom_text = note) def remove_note_text(self, txt, top = False): if top: note = self.top_note_text else: note = self.bottom_note_text if txt in note: note = note.replace(txt,'') if top: self.set_note_text_interactive(top_text = note) else: self.set_note_text_interactive(bottom_text = note) 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 note_focus_in(self, win, evt): if (self.timer): self.timer.resume_timing() def note_focus_out(self, wgt, evt): if (self.timer): self.timer.pause_timing() 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.WindowPosition.MOUSE) w.set_size_request(alloc.width, alloc.height/2) if self.parent_win: w.set_transient_for(self.parent_win) 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-in-event', self.note_focus_in) e.connect('focus-out-event', lambda e, ev, w: w.destroy(), w) e.connect('focus-out-event', self.note_focus_out) e.connect('activate', lambda e, w: w.destroy(), w) _, x, y = self.get_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() newval = num_selector.get_value() if newval: self.set_text_interactive(str(newval)) else: self.set_text_interactive('') def show_number_picker (self): w = Gtk.Window(type = Gtk.WindowType.POPUP) ns = NumberSelector(upper = self.upper, default = self.get_value()) ns.connect('changed', self.number_changed_cb) w.grab_focus() w.add(ns) _, xorigin, yorigin = self.get_window().get_origin() x, y = (self.get_allocated_width(), self.get_allocated_height()) popupx, popupy = w.get_size() overlapx = popupx-x overlapy = popupy-y w.move(xorigin - (overlapx/2), yorigin - (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 self._top_note_layout.set_font_description(font) self._bottom_note_layout.set_font_description(font) 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 show_note_text (self): '''Display the notes for the current view ''' self.top_note_text = self.get_note_display(self.top_note_list)[1] self._top_note_layout.set_markup(self.get_note_display(self.top_note_list)[2], -1) self.bottom_note_text = self.get_note_display(self.bottom_note_list)[1] self._bottom_note_layout.set_markup(self.get_note_display(self.bottom_note_list)[2], -1) self.queue_draw() def set_note_text (self, top_text = None, bottom_text = None, for_hint = False): '''Change the notes ''' if top_text is not None: self.update_notelist(self.top_note_list, top_text) if bottom_text is not None: self.update_notelist(self.bottom_note_list, bottom_text, for_hint) self.show_note_text() def set_note_text_interactive (self, *args, **kwargs): self.emit('notes-about-to-change') self.set_note_text(*args, **kwargs) self.emit('notes-changed') def set_notelist(self, top_notelist, bottom_notelist): '''Assign new note lists ''' if top_notelist: self.top_note_list = top_notelist if bottom_notelist: self.bottom_note_list = bottom_notelist def get_note_display(self, notelist, tracker_id = None, include_untracked = True): '''Parse a notelist for display Parse a notelist for the display. notelist - This method works on one notelist at a time, so top_note_list or bottom_note_list must be passed in. tracker_id - can specify a particular tracker. The default is to use tracker that is currently showing. include_untracked - When set to True(default), the untracked notes will be included in the output. Set it to false to exclude untracked notes. The output is returned in 3 formats: display_list - is tuple list in the format (notelist_index, tid, note) notelist_index - the index within the notelist tid - tracker id note - value of the note display_text - vanilla string representing all the values markup_text - pango markup string that colors each note for its tracker ''' display_list = [] display_text = '' markup_text = '' if tracker_id == None: tracker_id = self.tinfo.showing_tracker if include_untracked: track_filter = [tracker_info.NO_TRACKER, tracker_id] else: track_filter = [tracker_id] last_tracker = tracker_info.NO_TRACKER for notelist_index, (tid, note) in enumerate(notelist[:]): if tid not in track_filter: continue display_list.append((notelist_index, tid, note)) display_text += note if tid != last_tracker: if self.tinfo.get_color_markup(last_tracker): markup_text += '' if self.tinfo.get_color_markup(tid): markup_text += '' last_tracker = tid markup_text += note if self.tinfo.get_color_markup(last_tracker): markup_text += '' return((display_list, display_text, markup_text)) def update_notelist(self, notelist, new_notes, for_hint = False): '''Parse notes for a notelist A notelist stores individual notes in the format (tracker, note). The sequence is also meaningful - it dictates the order in which the notes are displayed. One notelist is maintained for the top notes(top_note_list), and one for the bottom(bottom_note_list). This method is responsible for maintaining those lists. When updating for hints(for_hint == True), the old notes are replaced completely by the new notes and set with NO_TRACKER. ''' # Remove any duplicates unique_notes = "" for note in new_notes: if note not in unique_notes: unique_notes += note # Create a list and text version of the notelist display_list = self.get_note_display(notelist)[0] display_text = self.get_note_display(notelist)[1] if display_text == unique_notes: return # Remove deleted values from the notelist del_offset = 0 for display_index, (notelist_index, tid, old_note) in enumerate(display_list[:]): if old_note not in unique_notes or for_hint: del notelist[notelist_index + del_offset] del display_list[display_index + del_offset] del_offset -= 1 else: # Adjust the display_list index display_list[display_index + del_offset] = (notelist_index + del_offset, tid, old_note) # Insert any new values into the notelist ins_offset = 0 display_index = 0 for new_index, new_note in enumerate(unique_notes): add_note = False # if the new notes are longer than the current ones - append if len(display_list) <= display_index: notelist_index = len(notelist) ins_offset = 0 add_note = True # Otherwise - advance until we find the appropriate place to insert else: old_note = display_list[display_index][2] if new_note != old_note: notelist_index = display_list[display_index][0] add_note = True display_index += 1 if add_note: if for_hint: use_tracker = tracker_info.NO_TRACKER else: use_tracker = self.tinfo.current_tracker notelist.insert(notelist_index + ins_offset, (use_tracker, new_note)) display_list.insert(new_index, (notelist_index + ins_offset, self.tinfo.current_tracker, new_note)) ins_offset = ins_offset + 1 self.trim_untracked_notes(notelist) def trim_untracked_notes(self, notelist): untracked_text = self.get_note_display(notelist, tracker_info.NO_TRACKER)[1] for tid, note in notelist[:]: if note in untracked_text and tid != tracker_info.NO_TRACKER: notelist.remove((tid, note)) def get_notes_for_undo(self): '''Return the top and bottom notelists ''' return((self.top_note_list[:], self.bottom_note_list[:])) def set_notes_for_undo(self, notelists): '''Reset the top and bottom notelists from an undo ''' self.top_note_list, self.bottom_note_list = notelists self.show_note_text() @simple_debug def do_draw(self, cr): w = self.get_allocated_width() h = self.get_allocated_height() style_ctx = self.get_style_context() self.draw_background_color(cr, style_ctx, w, h) if self.is_focus(): self.draw_highlight_box(cr, style_ctx, w, h) if self.border_color is not None: border_width = 3.0 cr.set_source_rgb(*self.border_color) cr.rectangle(border_width*0.5, border_width*0.5, w-border_width, h-border_width) cr.set_line_width(border_width) cr.stroke() if h < w: scale = h/float(BASE_SIZE) else: scale = w/float(BASE_SIZE) cr.scale(scale, scale) self.draw_text(cr, style_ctx) if self.draw_boxes and self.is_focus(): self.draw_note_area_highlight_box(cr, style_ctx) def draw_background_color (self, cr, style_ctx, w, h): 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.StateFlags.INSENSITIVE]) #Gdk.cairo_set_source_rgba( debug_set_color_rgba( cr, style_ctx.get_color(Gtk.StateFlags.INSENSITIVE)) elif self.is_focus(): #cr.set_source_color(self.style.base[Gtk.StateFlags.SELECTED]) #Gdk.cairo_set_source_rgba( debug_set_color_rgba( cr, style_ctx.get_color(Gtk.StateFlags.SELECTED)) elif self.custom_background_color: cr.set_source_rgb(*self.custom_background_color) else: #cr.set_source_color( # self.style.base[self.state] # ) #Gdk.cairo_set_source_rgba( cr.set_source_rgb(1.0, 1.0, 1.0) cr.rectangle( 0, 0, w, h, ) cr.fill() def draw_highlight_box (self, cr, style_ctx, w, h): #cr.set_source_color( # self.style.base[Gtk.StateFlags.SELECTED] # ) #Gdk.cairo_set_source_rgba( debug_set_color_rgba( cr, style_ctx.get_background_color(Gtk.StateFlags.SELECTED)) border = 4 * w / BASE_SIZE cr.rectangle( # left-top border*0.5, border*0.5, # bottom-right w-border, h-border, ) cr.set_line_width(border) cr.stroke() def draw_note_area_highlight_box (self, cr, style_ctx): # set up our paint brush... #cr.set_source_color( # self.style.mid[self.state] # ) #Gdk.cairo_set_source_rgba( debug_set_color_rgba( cr, style_ctx.get_border_color(self.get_state_flags())) cr.set_line_width(NORMAL_LINE_WIDTH) # 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, style_ctx): fontw, fonth = self._layout.get_pixel_size() # Draw a shadow for tracked conflicts. This is done to # differentiate between tracked and untracked conflicts. if self.shadow_color: cr.set_source_rgb(*self.shadow_color) for xoff, yoff in [(1,1),(2,2)]: cr.move_to((BASE_SIZE/2)-(fontw/2) + xoff, (BASE_SIZE/2) - (fonth/2) + yoff) PangoCairo.show_layout(cr, self._layout) if self.text_color: cr.set_source_rgb(*self.text_color) elif self.read_only: #cr.set_source_color(self.style.text[Gtk.StateFlags.NORMAL]) #Gdk.cairo_set_source_rgba( debug_set_color_rgba( cr, style_ctx.get_color(Gtk.StateFlags.NORMAL)) else: #cr.set_source_color(self.style.text[self.state]) #Gdk.cairo_set_source_rgba( debug_set_color_rgba( cr, style_ctx.get_color(Gtk.StateFlags.NORMAL)) # And draw the text in the middle of the allocated space if self._layout: cr.move_to( (BASE_SIZE/2)-(fontw/2), (BASE_SIZE/2) - (fonth/2), ) PangoCairo.update_layout(cr, self._layout) PangoCairo.show_layout(cr, self._layout) #cr.set_source_color(self.style.text[self.state]) #Gdk.cairo_set_source_rgba( debug_set_color_rgba( cr, style_ctx.get_color(Gtk.StateFlags.NORMAL)) # 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, ) PangoCairo.update_layout(cr, self._top_note_layout) PangoCairo.show_layout(cr, 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, ) PangoCairo.update_layout(cr, self._bottom_note_layout) PangoCairo.show_layout(cr, self._bottom_note_layout) def set_text_color (self, color, shadow = None): self.shadow_color = shadow self.text_color = color self.queue_draw() def set_background_color (self, color): self.custom_background_color = color self.queue_draw() def set_border_color (self, color): self.border_color = color self.queue_draw() def hide_notes (self): pass def show_notes (self): pass 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 tracker_id = None error_color = (1.0, 0, 0) highlight_color = ERROR_HIGHLIGHT_COLOR def set_value(self, val, tracker_id = None): if tracker_id == None: self.tracker_id = self.tinfo.current_tracker else: self.tracker_id = tracker_id self.normal_color = self.tinfo.get_color(self.tracker_id) self.set_text_color(self.normal_color) super(SudokuNumberBox, self).set_value(val) def get_value_for_undo(self): return(self.tracker_id, self.get_value(), self.tinfo.get_trackers_for_cell(self.x, self.y)) def set_value_for_undo (self, undo_val): tracker_id, value, all_traces = undo_val # When undo sets a value, switch to that tracker if value: self.tinfo.ui.select_tracker(tracker_id) self.set_value(value, tracker_id) self.tinfo.reset_trackers_for_cell(self.x, self.y, all_traces) self.emit('undo_change') def recolor(self, tracker_id): self.normal_color = self.tinfo.get_color(tracker_id) self.set_text_color(self.normal_color) def set_error_highlight (self, val): if val: if (self.tracker_id != tracker_info.NO_TRACKER): self.set_text_color(self.error_color, self.normal_color) else: self.set_text_color(self.error_color) 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: if not self.get_text(): self.set_text('X') self.set_text_color(self.error_color) elif self.get_text() == 'X': self.set_text('') self.set_text_color(self.normal_color) self.queue_draw() 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()