[gnome-games] Tracker overhaul
- From: Robert Ancell <rancell src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-games] Tracker overhaul
- Date: Tue, 25 May 2010 01:19:51 +0000 (UTC)
commit 01329b9edb8c0f6d787b8fb7f6d7fb76698165b5
Author: Jim Ross <jimbo dimensia com>
Date: Tue May 25 10:55:36 2010 +1000
Tracker overhaul
gnome-sudoku/data/tracker.ui | 259 ++++++++++++++++-------
gnome-sudoku/src/lib/gsudoku.py | 376 +++++++++++++++++++---------------
gnome-sudoku/src/lib/main.py | 281 ++++++++++++++++++++------
gnome-sudoku/src/lib/number_box.py | 243 ++++++++++++++++++----
gnome-sudoku/src/lib/saver.py | 52 +++--
gnome-sudoku/src/lib/tracker_info.py | 208 +++++++++++++++++++
6 files changed, 1050 insertions(+), 369 deletions(-)
---
diff --git a/gnome-sudoku/data/tracker.ui b/gnome-sudoku/data/tracker.ui
index 2f6b876..61c738a 100644
--- a/gnome-sudoku/data/tracker.ui
+++ b/gnome-sudoku/data/tracker.ui
@@ -13,25 +13,11 @@
<object class="GtkVBox" id="vbox1">
<property name="visible">True</property>
<child>
- <object class="GtkLabel" id="label3">
- <property name="visible">True</property>
- <property name="xalign">0</property>
- <property name="label" translatable="yes">_Trackers</property>
- <property name="use_underline">True</property>
- <property name="mnemonic_widget">treeview1</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
<object class="GtkAlignment" id="alignment3">
<property name="visible">True</property>
<property name="top_padding">12</property>
<property name="bottom_padding">12</property>
- <property name="right_padding">12</property>
+ <property name="right_padding">0</property>
<child>
<object class="GtkVBox" id="vbox2">
<property name="visible">True</property>
@@ -58,7 +44,7 @@
</child>
</object>
<packing>
- <property name="position">1</property>
+ <property name="position">0</property>
</packing>
</child>
<child>
@@ -67,109 +53,222 @@
<property name="xalign">0</property>
<property name="xscale">0</property>
<child>
- <object class="GtkVButtonBox" id="hbuttonbox1">
+ <object class="GtkHBox" id="bhbox1">
<property name="visible">True</property>
- <property name="spacing">6</property>
- <property name="layout_style">end</property>
+ <property name="spacing">2</property>
<child>
- <object class="GtkButton" id="AddTrackerButton">
+ <object class="GtkVButtonBox" id="hbuttonbox1">
<property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="can_default">True</property>
- <property name="receives_default">False</property>
+ <property name="spacing">6</property>
+ <property name="layout_style">end</property>
<child>
- <object class="GtkAlignment" id="alignment6">
+ <object class="GtkButton" id="AddTrackerButton">
<property name="visible">True</property>
- <property name="xscale">0</property>
- <property name="yscale">0</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="receives_default">False</property>
<child>
- <object class="GtkHBox" id="hbox4">
+ <object class="GtkAlignment" id="alignment6">
<property name="visible">True</property>
- <property name="spacing">2</property>
+ <property name="xscale">0</property>
+ <property name="yscale">0</property>
<child>
- <object class="GtkImage" id="image4">
+ <object class="GtkHBox" id="hbox4">
<property name="visible">True</property>
- <property name="stock">gtk-add</property>
- <property name="icon-size">4</property>
+ <property name="spacing">2</property>
+ <child>
+ <object class="GtkImage" id="image4">
+ <property name="visible">True</property>
+ <property name="stock">gtk-add</property>
+ <property name="icon-size">4</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label6">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">_Add</property>
+ <property name="use_underline">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
</object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">0</property>
- </packing>
</child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="ApplyTrackerButton">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="receives_default">False</property>
+ <child>
+ <object class="GtkAlignment" id="alignment8">
+ <property name="visible">True</property>
+ <property name="xscale">0</property>
+ <property name="yscale">0</property>
<child>
- <object class="GtkLabel" id="label6">
+ <object class="GtkHBox" id="hbox5">
<property name="visible">True</property>
- <property name="label" translatable="yes">_Add Tracker</property>
- <property name="use_underline">True</property>
+ <property name="spacing">2</property>
+ <child>
+ <object class="GtkImage" id="image5">
+ <property name="visible">True</property>
+ <property name="stock">gtk-apply</property>
+ <property name="icon-size">4</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label7">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">A_pply</property>
+ <property name="use_underline">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
</object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">1</property>
- </packing>
</child>
</object>
</child>
</object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
</child>
</object>
- <packing>
- <property name="expand">True</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
</child>
<child>
- <object class="GtkButton" id="ClearTrackerButton">
+ <object class="GtkVButtonBox" id="hbuttonbox2">
<property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="can_default">True</property>
- <property name="receives_default">False</property>
+ <property name="spacing">6</property>
+ <property name="layout_style">end</property>
<child>
- <object class="GtkAlignment" id="alignment5">
+ <object class="GtkButton" id="RemoveTrackerButton">
<property name="visible">True</property>
- <property name="xscale">0</property>
- <property name="yscale">0</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="receives_default">False</property>
<child>
- <object class="GtkHBox" id="hbox3">
+ <object class="GtkAlignment" id="alignment5">
<property name="visible">True</property>
- <property name="spacing">2</property>
+ <property name="xscale">0</property>
+ <property name="yscale">0</property>
<child>
- <object class="GtkImage" id="image3">
+ <object class="GtkHBox" id="hbox3">
<property name="visible">True</property>
- <property name="stock">gtk-clear</property>
- <property name="icon-size">4</property>
+ <property name="spacing">2</property>
+ <child>
+ <object class="GtkImage" id="image3">
+ <property name="visible">True</property>
+ <property name="stock">gtk-delete</property>
+ <property name="icon-size">4</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label5">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">_Remove</property>
+ <property name="use_underline">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
</object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">0</property>
- </packing>
</child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="HideTrackerButton">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="receives_default">False</property>
+ <child>
+ <object class="GtkAlignment" id="alignment4">
+ <property name="visible">True</property>
+ <property name="xscale">0</property>
+ <property name="yscale">0</property>
<child>
- <object class="GtkLabel" id="label5">
+ <object class="GtkHBox" id="hbox2">
<property name="visible">True</property>
- <property name="label" translatable="yes">_Clear Tracker</property>
- <property name="use_underline">True</property>
+ <property name="spacing">2</property>
+ <child>
+ <object class="GtkImage" id="image2">
+ <property name="visible">True</property>
+ <property name="stock">gtk-close</property>
+ <property name="icon-size">4</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label4">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">H_ide</property>
+ <property name="use_underline">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
</object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">1</property>
- </packing>
</child>
</object>
</child>
</object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
</child>
</object>
- <packing>
- <property name="expand">True</property>
- <property name="fill">True</property>
- <property name="position">1</property>
- </packing>
</child>
</object>
</child>
@@ -177,7 +276,7 @@
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
- <property name="position">2</property>
+ <property name="position">1</property>
</packing>
</child>
</object>
diff --git a/gnome-sudoku/src/lib/gsudoku.py b/gnome-sudoku/src/lib/gsudoku.py
index d9a5bb0..ba8f1ec 100644
--- a/gnome-sudoku/src/lib/gsudoku.py
+++ b/gnome-sudoku/src/lib/gsudoku.py
@@ -2,24 +2,10 @@
import gtk, gobject
import colors
import math
-import random
from simple_debug import simple_debug
import sudoku
import number_box
-
-TRACKER_COLORS = [
- # Use tango colors recommended here:
- # http://tango.freedesktop.org/Tango_Icon_Theme_Guidelines
- tuple([x / 255.0 for x in cols]) for cols in
- [(32, 74, 135), # Sky Blue 3
- (78, 154, 6), # Chameleon 3
- (206, 92, 0), # Orange 3
- (143, 89, 2), # Chocolate 3
- (92, 53, 102), # Plum 3
- (85, 87, 83), # Aluminium 5
- (196, 160, 0), # Butter 3
- ]
- ]
+import tracker_info
def gtkcolor_to_rgb (color):
return (color.red / float(2**16),
@@ -100,13 +86,13 @@ class SudokuGameDisplay (SudokuNumberGrid, gobject.GObject):
self.impossible_hints = 0
self.impossibilities = []
self.trackers = {}
- self.__trackers_tracking__ = {}
+ self.tinfo = tracker_info.TrackerInfo()
gobject.GObject.__init__(self)
SudokuNumberGrid.__init__(self, group_size = group_size)
self.setup_grid(grid, group_size)
for e in self.__entries__.values():
e.show()
- e.connect('undo-change', self.entry_callback)
+ e.connect('undo-change', self.entry_callback, 'undo-change')
e.connect('changed', self.entry_callback)
e.connect('focus-in-event', self.focus_callback)
e.connect('key-press-event', self.key_press_cb)
@@ -219,68 +205,127 @@ class SudokuGameDisplay (SudokuNumberGrid, gobject.GObject):
''.join([str(v) for v in vals])
txt = ''.join([str(v) for v in vals])
if txt != entry.get_text():
- set_method(bottom_text = txt)
+ set_method(bottom_text = txt, for_hint = True)
self.hints += 1
elif not entry.get_text():
if entry.get_text() != 'X':
self.hints += 1
- set_method(bottom_text = 'X')
+ set_method(bottom_text = 'X', for_hint = True)
else:
- set_method(bottom_text = "")
+ set_method(bottom_text = "", for_hint = True)
@simple_debug
def reset_grid (self):
- """Reset grid to its original setup.
+ '''Remove all untracked values from the grid
- Return a list of items we removed so that callers can handle
- e.g. Undo properly"""
+ This method is used to clear all untracked values from the grid for
+ the undo processing. The tracked values and notes are handled higher
+ up by the caller.
+ '''
removed = []
for x in range(self.group_size):
for y in range(self.group_size):
if not self.grid.virgin._get_(x, y):
- val = self.__entries__[(x, y)].get_value() # get the value from the user-visible grid,
- if val:
- removed.append((x, y, val, self.trackers_for_point(x, y, val)))
- self.remove(x, y, do_removal = True)
+ e = self.__entries__[(x, y)]
+ val = e.get_value()
+ track = e.tracker_id
+ if val and track == tracker_info.NO_TRACKER:
+ removed.append((x, y, val))
+ self.remove(x, y)
return removed
- def clear_notes (self, side = 'Both'):
+ def clear_notes (self, side = 'Both', tracker = None):
'''Remove notes
The list of notes removed by this function are returned in a list.
+ The notes are returned in the format (x, y, (side, pos, tid, note)) where:
+ x and y are the cell's coordinates
+ side is either 'Top' or 'Bottom'
+ pos is the index of the note within the notelist
+ tid is the tracker id for the note
+ note is the value of the note
+
The side argument determines what notes get cleared as well as what
notes get returned.
- 'Both' - Clears both the top and bottom notes
+ 'Both' - Clears both the top and bottom notes(default)
'Top' - Clear only the top notes
'Bottom' - Clear only the bottom notes
+ 'AutoHint' - Clear all bottom notes for all trackers
+ 'All' - Reset all notes
+
+ For 'Top', 'Bottom', and 'Both', the tracker argument can be supplied
+ to clear for a specific tracker. Set tracker to None(default) to
+ operate on just what is currently displayed.
'''
- # Set the argument list for NumberBox.set_note_text()
- if side == 'Both':
- clear_args = {'top_text':'', 'bottom_text':''}
- elif side == 'Top':
- clear_args = {'top_text':''}
- else:
- clear_args = {'bottom_text':''}
# Storage for removed notes
removed = []
for x in range(self.group_size):
for y in range(self.group_size):
e = self.__entries__[(x, y)]
- top, bottom = e.get_note_text()
- # Don't return the bottom notes if we're only clearing the top
- # or the top notes if we're only clearing the bottom.
- if side == 'Top':
- bottom = ''
- elif side == 'Bottom':
- top = ''
- if top or bottom:
- removed.append((x, y, (top, bottom)))
- e.set_note_text(**clear_args)
- e.queue_draw()
+ if side in ['Top', 'Both']:
+ if tracker == None:
+ top_display_list = e.get_note_display(e.top_note_list)[0]
+ else:
+ top_display_list = e.get_note_display(e.top_note_list, tracker, False)[0]
+ for offset, (notelist_index, tracker_id, note) in enumerate(top_display_list):
+ removed.append((x, y, ('Top', notelist_index, tracker_id, note)))
+ del e.top_note_list[notelist_index - offset]
+ if side in ['Bottom', 'Both']:
+ if tracker == None:
+ bottom_display_list = e.get_note_display(e.bottom_note_list)[0]
+ else:
+ bottom_display_list = e.get_note_display(e.bottom_note_list, tracker, False)[0]
+ for offset, (notelist_index, tracker_id, note) in enumerate(bottom_display_list):
+ removed.append((x, y, ('Bottom', notelist_index, tracker_id, note)))
+ del e.bottom_note_list[notelist_index - offset]
+ if side == 'All':
+ for notelist_index, (tracker_id, note) in enumerate(e.top_note_list):
+ removed.append((x, y, ('Top', notelist_index, tracker_id, note)))
+ e.top_note_list = []
+ if side in ['All', 'AutoHint']:
+ for notelist_index, (tracker_id, note) in enumerate(e.bottom_note_list):
+ removed.append((x, y, ('Bottom', notelist_index, tracker_id, note)))
+ e.bottom_note_list = []
+ # Redraw the notes
+ self.update_all_notes()
return removed
+ def apply_notelist(self, notelist, apply_tracker = False):
+ '''Re-apply notes
+
+ Re-apply notes that have been removed with the clear_notes() function.
+ The apply_tracker argument is used for the "Apply Tracker" button
+ functionality, which requires the history to be updated.
+ '''
+ for x, y, (side, notelist_index, tracker_id, note) in notelist:
+ cell = self.__entries__[x, y]
+ if apply_tracker:
+ use_tracker = tracker_info.NO_TRACKER
+ cell.emit('notes-about-to-change')
+ else:
+ use_tracker = tracker_id
+ if side == 'Top':
+ cell.top_note_list.insert(notelist_index, (use_tracker, note))
+ if side == 'Bottom':
+ cell.bottom_note_list.insert(notelist_index, (use_tracker, note))
+ if apply_tracker:
+ cell.emit('notes-changed')
+ # When applying a tracker - update the notes to remove
+ # duplicates from other trackers.
+ if side == 'Top':
+ cell.trim_untracked_notes(cell.top_note_list)
+ else:
+ cell.trim_untracked_notes(cell.bottom_note_list)
+ # Redraw the notes
+ self.update_all_notes()
+
@simple_debug
def blank_grid (self):
+ '''Wipe out everything on the grid.
+
+ This blanks all values, notes, tracked values, virgin values. You end
+ up with a blank grid ready for a new puzzle.
+ '''
for x in range(self.group_size):
for y in range(self.group_size):
e = self.__entries__[(x, y)]
@@ -291,14 +336,13 @@ class SudokuGameDisplay (SudokuNumberGrid, gobject.GObject):
self.__entries__[imp_cell].set_text('')
self.impossibilities = []
self.grid = None
- self.clear_notes()
+ self.clear_notes('All')
+ self.tinfo.reset()
@simple_debug
def change_grid (self, grid, group_size):
self.hints = 0
self.impossible_hints = 0
- self.trackers = {}
- self.__trackers_tracking__ = {}
self.blank_grid()
self.setup_grid(grid, group_size)
@@ -342,11 +386,12 @@ class SudokuGameDisplay (SudokuNumberGrid, gobject.GObject):
@simple_debug
def entry_callback (self, widget, *args):
if not widget.get_text():
- if self.grid and self.grid._get_(widget.x, widget.y):
- self.grid.remove(widget.x, widget.y)
- self.remove(widget.x, widget.y)
+ self.remove(widget.x, widget.y, *args)
+ # Trackers need to be redisplayed on an undo
+ if args and args[0] == 'undo-change':
+ self.show_track()
else:
- self.entry_validate(widget)
+ self.entry_validate(widget, *args)
def update_all_hints (self):
for x in range(self.group_size):
@@ -359,10 +404,26 @@ class SudokuGameDisplay (SudokuNumberGrid, gobject.GObject):
else:
self.show_hint_for_entry(e)
+ def update_all_notes (self):
+ '''Display the notes for all the cells
+
+ The notes are context sensitive to the trackers. This method displays
+ all of the notes for the currently viewed selection.
+ '''
+ for x in range(self.group_size):
+ for y in range(self.group_size):
+ self.__entries__[(x, y)].show_note_text()
+
@simple_debug
def entry_validate (self, widget, *args):
val = widget.get_value()
- self.add_value(widget.x, widget.y, val)
+ if (args and args[0] == 'undo-change'):
+ # When undoing from one value to another - remove the errors from
+ # the previous value and add the new value to the proper tracker
+ self.remove_error_highlight()
+ self.add_value(widget.x, widget.y, val, widget.tracker_id)
+ else:
+ self.add_value(widget.x, widget.y, val)
if self.grid.check_for_completeness():
self.emit('puzzle-finished')
@@ -382,36 +443,45 @@ class SudokuGameDisplay (SudokuNumberGrid, gobject.GObject):
for coord in self.grid.conflicts[(x, y)]:
self.__entries__[coord].set_error_highlight(True)
- @simple_debug
- def add_value (self, x, y, val, trackers = []):
- """Add value val at position x, y.
-
- If tracker is True, we track it with tracker ID tracker.
+ def set_value(self, x, y, val):
+ '''Sets value for position x, y to val.
- Otherwise, we use any currently tracking trackers to track our addition.
+ Calls set_text_interactive so the history list is updated.
+ '''
+ self.__entries__[(x, y)].set_text_interactive(str(val))
- Providing the tracker arg is mostly useful for e.g. undo/redo
- or removed items.
+ @simple_debug
+ def add_value (self, x, y, val, tracker = None):
+ """Add value val at position x, y.
- To specify NO trackers, use trackers = [-1]
+ If tracker is set, we track the value with it. Otherwise,
+ the current tracker is used(default).
"""
- # Add the value to the UI to display
- self.__entries__[(x, y)].set_value(val)
- if self.doing_initial_setup:
- self.__entries__[(x, y)].set_read_only(True)
- # Handle any trackers.
- if trackers:
- # Explicitly specified tracker
- for tracker in trackers:
- if tracker == -1:
- pass
- self.__entries__[(x, y)].set_color(self.get_tracker_color(tracker))
- self.trackers[tracker].append((x, y, val))
- elif True in self.__trackers_tracking__.values():
- for k, v in self.__trackers_tracking__.items():
- if v:
- self.__entries__[(x, y)].set_color(self.get_tracker_color(k))
- self.trackers[k].append((x, y, val))
+ # If the cell already has a value - remove it first.
+ e = self.__entries__[(x, y)]
+ if e.get_value():
+ self.remove(x, y)
+ # Explicitly specified tracker
+ if tracker:
+ # Only add it to the display when it's tracker is visible
+ if tracker == tracker_info.NO_TRACKER or tracker == self.tinfo.showing_tracker:
+ self.__entries__[(x, y)].set_value(val, tracker)
+ # If the tracker isn't showing at the moment - add it as a trace
+ if tracker != tracker_info.NO_TRACKER:
+ self.tinfo.add_trace(x, y, val, tracker)
+ else:
+ # Add a trace(tracked value) if a tracker is selected
+ if self.tinfo.current_tracker != tracker_info.NO_TRACKER:
+ self.tinfo.add_trace(x, y, val)
+ # Remove all tracked values(all traces) for the cell if the player
+ # adds an untracked value
+ else:
+ self.tinfo.remove_trace(x, y)
+ self.__entries__[(x, y)].set_value(val, self.tinfo.current_tracker)
+ if self.doing_initial_setup:
+ self.__entries__[(x, y)].set_read_only(True)
+ else:
+ self.__entries__[(x, y)].recolor(self.tinfo.current_tracker)
# Add it to the underlying grid
self.grid.add(x, y, val, True)
# Highlight any conflicts that the new value creates
@@ -425,27 +495,23 @@ class SudokuGameDisplay (SudokuNumberGrid, gobject.GObject):
self.mark_impossible_implications(x, y)
@simple_debug
- def remove (self, x, y, do_removal = False):
+ def remove (self, x, y, *args):
"""Remove x, y from our visible grid.
- If do_removal, remove it from our underlying grid as well.
+ *args is passed from the undo mechanism
"""
e = self.__entries__[(x, y)]
# Always call the grid's remove() for proper conflict resolution
if self.grid:
self.grid.remove(x, y)
self.remove_error_highlight()
- # remove trackers
- for t in self.trackers_for_point(x, y):
- remove = []
- for crumb in self.trackers[t]:
- if crumb[0] == x and crumb[1] == y:
- remove.append(crumb)
- for r in remove:
- self.trackers[t].remove(r)
- if e.get_text():
- e.set_value(0)
- e.unset_color()
+ # Remove it from the tracker. When removing via undo, the trace
+ # manipulation is handled at a higher level
+ if not args or args[0] != 'undo-change':
+ if e.tracker_id != tracker_info.NO_TRACKER:
+ self.tinfo.remove_trace(x, y, e.tracker_id)
+ # Reset the value and tracker id
+ e.set_value(0, tracker_info.NO_TRACKER)
# Update all hints if we need to
if self.grid and self.always_show_hints and not self.doing_initial_setup:
self.update_all_hints()
@@ -573,88 +639,64 @@ class SudokuGameDisplay (SudokuNumberGrid, gobject.GObject):
if grid_modified and self.always_show_hints:
self.update_all_hints()
- @simple_debug
- def create_tracker (self, identifier = 0):
- if not identifier:
- identifier = 0
- while self.trackers.has_key(identifier):
- identifier += 1
- self.trackers[identifier] = []
- return identifier
-
- def trackers_for_point (self, x, y, val = None):
- if val:
- # if we have a value we can do this a simpler way...
- track_for_point = filter(
- lambda t: (x, y, val) in t[1],
- self.trackers.items()
- )
- else:
- track_for_point = filter(
- lambda tkr: True in [t[0] == x and t[1] == y for t in tkr[1]],
- self.trackers.items())
- return [t[0] for t in track_for_point]
-
- def get_tracker_color (self, identifier):
- if len(TRACKER_COLORS)>identifier:
- return TRACKER_COLORS[identifier]
- else:
- random_color = TRACKER_COLORS[0]
- while random_color in TRACKER_COLORS:
- # If we have generated all possible colors, this will
- # enter an infinite loop
- random_color = (random.randint(0, 100)/100.0,
- random.randint(0, 100)/100.0,
- random.randint(0, 100)/100.0)
- TRACKER_COLORS.append(random_color)
- return self.get_tracker_color(identifier)
-
- @simple_debug
- def toggle_tracker (self, identifier, value):
- """Toggle tracking for tracker identified by identifier."""
- self.__trackers_tracking__[identifier] = value
+ def delete_by_tracker (self):
+ '''Delete all cells tracked by the current tracker
- def delete_by_tracker (self, identifier):
- """Delete all cells tracked by tracker ID identifer."""
+ The values are deleted from the tracker as well as the visible grid.
+ '''
ret = []
- while self.trackers[identifier]:
- x, y, v = self.trackers[identifier][0]
- ret.append((x, y, v, self.trackers_for_point(x, y, v)))
+ tracker = self.tinfo.get_tracker(self.tinfo.showing_tracker)
+ if not tracker:
+ return ret
+ for (x, y), value in tracker.items():
+ ret.append((x, y, value, self.tinfo.showing_tracker))
self.remove(x, y)
if self.grid and self.grid._get_(x, y):
self.grid.remove(x, y)
return ret
- def delete_except_for_tracker (self, identifier):
- tracks = self.trackers[identifier]
- removed = []
- for x in range(self.group_size):
- for y in range(self.group_size):
- val = self.grid._get_(x, y)
- if (val
- and (x, y, val) not in tracks
- and not self.grid.virgin._get_(x, y)
- ):
- removed.append((x, y, val, self.trackers_for_point(x, y, val)))
- self.remove(x, y)
- if self.grid and self.grid._get_(x, y):
- self.grid.remove(x, y)
+ def cover_track(self, hide = False):
+ '''Hide the current tracker
- return removed
+ All tracked values are deleted from the display, but kept by the
+ tracker. Setting hide to True changes prevents anything but untracked
+ values to be shown after the call.
+ '''
+ track = self.tinfo.get_tracker(self.tinfo.showing_tracker)
+ if track:
+ for coord in track.keys():
+ self.__entries__[coord].set_value(0, tracker_info.NO_TRACKER)
+ self.grid.remove(*coord)
+ self.remove_error_highlight()
+ self.mark_impossible_implications(*coord)
+ if hide:
+ self.tinfo.hide_tracker()
+ # Update all hints if we need to
+ if self.always_show_hints and not self.doing_initial_setup:
+ self.update_all_hints()
- def add_tracker (self, x, y, tracker, val = None):
- self.__entries__[(x, y)].set_color(self.get_tracker_color(tracker))
- # Highlight the conflicts when opening a saved game
- if self.grid.conflicts.has_key((x, y)):
- self.__entries__[(x, y)].set_error_highlight(True)
- if not val:
- val = self.grid._get_(x, y)
- self.trackers[tracker].append((x, y, val))
-
- def remove_tracker (self, x, y, tracker, val = None):
- if not val:
- val = self.grid._get_(x, y)
- self.trackers[tracker].remove((x, y, val))
+ def show_track(self):
+ '''Displays the current tracker items
+
+ The values and notes for the currently showing tracker will be
+ displayed
+ '''
+ track = self.tinfo.get_tracker(self.tinfo.showing_tracker)
+ if not track:
+ return
+ for (x, y), value in track.items():
+ self.__entries__[(x, y)].set_value(value, self.tinfo.showing_tracker)
+ self.__entries__[(x, y)].recolor(self.tinfo.showing_tracker)
+ # Add it to the underlying grid
+ self.grid.add(x, y, value, True)
+ # Highlight any conflicts that the new value creates
+ self.highlight_conflicts(x, y)
+ # Draw our entry
+ self.__entries__[(x, y)].queue_draw()
+ self.mark_impossible_implications(x, y)
+ # Update all hints if we need to
+ if self.always_show_hints and not self.doing_initial_setup:
+ self.update_all_hints()
if __name__ == '__main__':
window = gtk.Window()
diff --git a/gnome-sudoku/src/lib/main.py b/gnome-sudoku/src/lib/main.py
index bd89d89..1b19b97 100644
--- a/gnome-sudoku/src/lib/main.py
+++ b/gnome-sudoku/src/lib/main.py
@@ -10,6 +10,7 @@ import threading
import gobject
import gtk
+import pango
from gettext import gettext as _
from gettext import ngettext
@@ -20,6 +21,7 @@ import printing
import saver
import sudoku_maker
import timer
+import tracker_info
from defaults import (APPNAME, APPNAME_SHORT, AUTHORS, COPYRIGHT, DESCRIPTION, DOMAIN,
IMAGE_DIR, LICENSE, MIN_NEW_PUZZLES, UI_DIR, VERSION, WEBSITE, WEBSITE_LABEL)
from gtk_goodies import gconf_wrapper, Undo, dialog_extras
@@ -292,14 +294,15 @@ class UI (gconf_wrapper.GConfWrapper):
self.history = Undo.UndoHistoryList(undo_widg, redo_widg)
for entry in self.gsd.__entries__.values():
Undo.UndoableGenericWidget(entry, self.history,
- set_method = 'set_value_from_undo',
+ set_method = 'set_value_for_undo',
+ get_method = 'get_value_for_undo',
pre_change_signal = 'value-about-to-change'
)
Undo.UndoableGenericWidget(entry, self.history,
- set_method = 'set_notes',
- get_method = 'get_note_text',
+ set_method = 'set_notes_for_undo',
+ get_method = 'get_notes_for_undo',
signal = 'notes-changed',
- pre_change_signal = 'value-about-to-change',
+ pre_change_signal = 'notes-about-to-change',
)
def setup_color (self):
@@ -454,6 +457,7 @@ class UI (gconf_wrapper.GConfWrapper):
self.tracker_ui.reset()
self.history.clear()
self.won = False
+ self.old_tracker_view = None
@simple_debug
def resize_cb (self, widget, event):
@@ -513,13 +517,20 @@ class UI (gconf_wrapper.GConfWrapper):
clearer.perform()
def do_game_reset (self, *args):
+ self.gsd.cover_track()
+ self.cleared.append(self.tinfo.save())
self.cleared.append(self.gsd.reset_grid())
+ self.cleared_notes.append((tracker_info.NO_TRACKER, self.gsd.clear_notes('All')))
+ self.tinfo.reset()
self.stop_dancer()
- self.do_clear_notes()
def undo_game_reset (self, *args):
+ self.tracker_ui.select_tracker(tracker_info.NO_TRACKER)
for entry in self.cleared.pop():
self.gsd.add_value(*entry)
+ self.tinfo.load(self.cleared.pop())
+ self.tracker_ui.select_tracker(self.tinfo.current_tracker)
+ self.gsd.show_track()
self.undo_clear_notes()
def clear_top_notes_cb (self, *args):
@@ -538,20 +549,14 @@ class UI (gconf_wrapper.GConfWrapper):
)
clearer.perform()
- def do_clear_notes(self, side = 'Both'):
+ def do_clear_notes(self, side):
''' Clear top, bottom, or all notes - in undoable fashion
- The side argument is used to specify which notes
- are to be cleared.
- 'Top' - just clear the top notes
- 'Bottom' - just clear the bottom notes
- 'Both' - clear all notes(argument default)
-
- Store all of the cleared notes in the cleared_notes list so the undo
- can pick up on them later. The list items are in the format
- (x, y, (top note, bottom note)).
+ The side argument is used to specify which notes are to be cleared.
+ 'Top' - Clear only the top notes
+ 'Bottom' - Clear only the bottom notes
'''
- self.cleared_notes.append(self.gsd.clear_notes(side))
+ self.cleared_notes.append((self.tinfo.current_tracker, self.gsd.clear_notes(side)))
# Turn off auto-hint if the player clears the bottom notes
if side == 'Bottom' and self.gconf['always_show_hints']:
always_show_hint_wdgt = self.main_actions.get_action('AlwaysShowPossible')
@@ -563,19 +568,18 @@ class UI (gconf_wrapper.GConfWrapper):
def undo_clear_notes(self):
''' Undo previously cleared notes
- Clearing notes sets the cleared_notes list of tuples indicating the
- notes that were cleared. They are in the format
- (x, y, (top note ,bottom note))
+ Clearing notes fills the cleared_notes list of notes that were cleared.
'''
- for x, y, notes in self.cleared_notes.pop():
- top, bottom = notes
- if top:
- self.gsd.__entries__[x, y].set_note_text(top_text = top)
- if bottom:
- self.gsd.__entries__[x, y].set_note_text(bottom_text = bottom)
+ cleared_tracker, cleared_notes = self.cleared_notes.pop()
+ # Change the tracker selection if it was tracking during the clear
+ if cleared_tracker != tracker_info.NO_TRACKER:
+ self.tracker_ui.select_tracker(cleared_tracker)
+ self.gsd.apply_notelist(cleared_notes)
# Update the hints...in case we're undoing over top of them
if self.gconf['always_show_hints']:
self.gsd.update_all_hints()
+ # Redraw the notes
+ self.gsd.update_all_notes()
# Make sure we're still dancing if we undo after win
if self.gsd.grid.check_for_completeness():
self.start_dancer()
@@ -591,7 +595,7 @@ class UI (gconf_wrapper.GConfWrapper):
self.gsd.update_all_hints()
else:
self.gsd.always_show_hints = False
- self.gsd.clear_notes('Bottom')
+ self.gsd.clear_notes('AutoHint')
@simple_debug
def impossible_implication_cb (self, action):
@@ -602,17 +606,24 @@ class UI (gconf_wrapper.GConfWrapper):
@simple_debug
def setup_tracker_interface (self):
- self.trackers = {}
self.tracker_ui = TrackerBox(self)
self.tracker_ui.show_all()
self.tracker_ui.hide()
+ self.tinfo = tracker_info.TrackerInfo()
+ self.old_tracker_view = None
self.game_box.add(self.tracker_ui)
@simple_debug
def tracker_toggle_cb (self, widg):
if widg.get_active():
+ if self.old_tracker_view:
+ self.tinfo.set_tracker_view(self.old_tracker_view)
+ self.tracker_ui.select_tracker(self.tinfo.current_tracker)
+ self.gsd.show_track()
self.tracker_ui.show_all()
else:
+ self.old_tracker_view = self.tinfo.get_tracker_view()
+ self.tracker_ui.hide_tracker_cb(None)
self.tracker_ui.hide()
@simple_debug
@@ -708,6 +719,8 @@ class TrackerBox (gtk.VBox):
self.builder.set_translation_domain(DOMAIN)
self.builder.add_from_file(os.path.join(UI_DIR, 'tracker.ui'))
self.main_ui = main_ui
+ self.tinfo = tracker_info.TrackerInfo()
+ self.tinfo.ui = self
self.vb = self.builder.get_object('vbox1')
self.vb.unparent()
self.pack_start(self.vb, expand = True, fill = True)
@@ -721,45 +734,77 @@ class TrackerBox (gtk.VBox):
for tree in self.tracker_model:
if tree[0] > -1:
self.tracker_model.remove(tree.iter)
+ self.tinfo.reset()
+ self.tracker_actions.set_sensitive(False)
@simple_debug
def setup_tree (self):
self.tracker_tree = self.builder.get_object('treeview1')
self.tracker_model = gtk.ListStore(int, gtk.gdk.Pixbuf, str)
+ self.tracker_model.set_sort_column_id(0, gtk.SORT_ASCENDING)
self.tracker_tree.set_model(self.tracker_model)
col1 = gtk.TreeViewColumn("", gtk.CellRendererPixbuf(), pixbuf = 1)
- col2 = gtk.TreeViewColumn("", gtk.CellRendererText(), text = 2)
+ rend = gtk.CellRendererText()
+ col2 = gtk.TreeViewColumn("", rend, text = 2)
+ col2.set_cell_data_func(rend, self.draw_tracker_name)
self.tracker_tree.append_column(col2)
self.tracker_tree.append_column(col1)
# Our initial row...
- self.tracker_model.append([-1, None, _('No Tracker')])
+ self.tracker_model.append([-1, None, _('Untracked')])
self.tracker_tree.get_selection().connect('changed', self.selection_changed_cb)
@simple_debug
def setup_actions (self):
self.tracker_actions = gtk.ActionGroup('tracker_actions')
self.tracker_actions.add_actions(
- [('Clear',
+ [('Remove',
gtk.STOCK_CLEAR,
- _('_Clear Tracker'),
- None, _('Clear all moves tracked by selected tracker.'),
- self.clear_cb
+ _('_Remove'),
+ None, _('Delete selected tracker.'),
+ self.remove_tracker_cb
+ ),
+ ('Hide',
+ gtk.STOCK_CLEAR,
+ _('H_ide'),
+ None, _('Hide current tracker entries.'),
+ self.hide_tracker_cb
+ ),
+ ('Apply',
+ gtk.STOCK_CLEAR,
+ _('A_pply'),
+ None, _('Apply all tracked values and remove the tracker.'),
+ self.apply_tracker_cb
),
]
)
- a = self.tracker_actions.get_action('Clear')
- a.connect_proxy(self.builder.get_object('ClearTrackerButton'))
+ a = self.tracker_actions.get_action('Remove')
+ a.connect_proxy(self.builder.get_object('RemoveTrackerButton'))
+ a = self.tracker_actions.get_action('Hide')
+ a.connect_proxy(self.builder.get_object('HideTrackerButton'))
+ a = self.tracker_actions.get_action('Apply')
+ a.connect_proxy(self.builder.get_object('ApplyTrackerButton'))
self.builder.get_object('AddTrackerButton').connect('clicked',
self.add_tracker)
# Default to insensitive (they only become sensitive once a tracker is added)
self.tracker_actions.set_sensitive(False)
+ def draw_tracker_name(self, column, cell, model, iter):
+ if model.get_value(iter, 0) == self.tinfo.showing_tracker and \
+ self.tinfo.showing_tracker != tracker_info.NO_TRACKER and \
+ self.tinfo.showing_tracker != self.tinfo.current_tracker:
+ cell.set_property('underline', pango.UNDERLINE_DOUBLE)
+ else:
+ cell.set_property('underline', pango.UNDERLINE_NONE)
+
@simple_debug
- def add_tracker (self, *args):
- tracker_id = self.main_ui.gsd.create_tracker()
+ def add_tracker (self, *args, **keys):
+ if keys and keys.has_key('tracker_id'):
+ tracker_id = self.tinfo.create_tracker(keys['tracker_id'])
+ else:
+ tracker_id = self.tinfo.create_tracker()
pixbuf = self.pixbuf_transform_color(
STOCK_PIXBUFS['tracks'],
- self.main_ui.gsd.get_tracker_color(tracker_id),
+ self.tinfo.get_color(tracker_id)
)
# select our new tracker
self.tracker_tree.get_selection().select_iter(
@@ -768,6 +813,7 @@ class TrackerBox (gtk.VBox):
_("Tracker %s") % (tracker_id + 1)]
)
)
+ self.tinfo.set_tracker(tracker_id)
@simple_debug
def pixbuf_transform_color (self, pixbuf, color):
@@ -785,10 +831,28 @@ class TrackerBox (gtk.VBox):
pixbuf.get_width(), pixbuf.get_height(), pixbuf.get_rowstride())
@simple_debug
- def select_tracker (self, tracker_id):
+ def find_tracker (self, tracker_id):
for row in self.tracker_model:
if row[0] == tracker_id:
- self.tracker_tree.get_selection().select_iter(row.iter)
+ return row
+ return None
+
+ @simple_debug
+ def select_tracker (self, tracker_id):
+ track_row = self.find_tracker(tracker_id)
+ if track_row:
+ self.tracker_tree.get_selection().select_iter(track_row.iter)
+ self.tinfo.set_tracker(tracker_id)
+
+ def redraw_row(self, tracker_id):
+ track_row = self.find_tracker(tracker_id)
+ if track_row:
+ self.tracker_model.row_changed(self.tracker_model.get_path(track_row.iter), track_row.iter)
+
+ def set_tracker_action_sense(self, enabled):
+ self.tracker_actions.set_sensitive(True)
+ for action in self.tracker_actions.list_actions():
+ action.set_sensitive(self.tinfo.showing_tracker != tracker_info.NO_TRACKER)
@simple_debug
def selection_changed_cb (self, selection):
@@ -796,32 +860,129 @@ class TrackerBox (gtk.VBox):
if itr:
selected_tracker_id = mod.get_value(itr, 0)
else:
- selected_tracker_id = -1
- # This should be cheap since we don't expect many trackers...
- # We cycle through each row and toggle it off if it's not
- # selected; on if it is selected
- for row in self.tracker_model:
- tid = row[0]
- if tid != -1: # -1 == no tracker
- self.main_ui.gsd.toggle_tracker(tid, tid == selected_tracker_id)
- self.tracker_actions.set_sensitive(selected_tracker_id != -1)
-
- @simple_debug
- def clear_cb (self, action):
+ selected_tracker_id = tracker_info.NO_TRACKER
+ if selected_tracker_id != tracker_info.NO_TRACKER:
+ self.main_ui.gsd.cover_track()
+ # Remove the underline on the showing_tracker
+ self.redraw_row(self.tinfo.showing_tracker)
+ self.tinfo.set_tracker(selected_tracker_id)
+ self.set_tracker_action_sense(self.tinfo.showing_tracker != tracker_info.NO_TRACKER)
+ # Show the tracker
+ if selected_tracker_id != tracker_info.NO_TRACKER:
+ self.main_ui.gsd.show_track()
+ self.main_ui.gsd.update_all_notes()
+ if self.main_ui.gconf['always_show_hints']:
+ self.main_ui.gsd.update_all_hints()
+
+ @simple_debug
+ def remove_tracker_cb (self, action):
mod, itr = self.tracker_tree.get_selection().get_selected()
# This should only be called if there is an itr, but we'll
# double-check just in case.
if itr:
- selected_tracker_id = mod.get_value(itr, 0)
- self.tracker_delete_tracks(selected_tracker_id)
+ clearer = Undo.UndoableObject(
+ self.do_delete_tracker,
+ self.undo_delete_tracker,
+ self.main_ui.history
+ )
+ clearer.perform()
@simple_debug
- def tracker_delete_tracks (self, tracker_id):
- clearer = Undo.UndoableObject(
- lambda *args: self.main_ui.cleared.append(self.main_ui.gsd.delete_by_tracker(tracker_id)),
- lambda *args: [self.main_ui.gsd.add_value(*entry) for entry in self.main_ui.cleared.pop()],
- self.main_ui.history)
- clearer.perform()
+ def hide_tracker_cb (self, action):
+ hiding_tracker = self.tinfo.showing_tracker
+ self.select_tracker(tracker_info.NO_TRACKER)
+ self.main_ui.gsd.cover_track(True)
+ self.main_ui.gsd.update_all_notes()
+ self.set_tracker_action_sense(False)
+ self.redraw_row(hiding_tracker)
+
+ @simple_debug
+ def apply_tracker_cb (self, action):
+ '''Apply Tracker button action
+ '''
+ # Shouldn't be here if no tracker is showing
+ if self.tinfo.showing_tracker == tracker_info.NO_TRACKER:
+ return
+ # Apply the tracker in undo-able fashion
+ applyer = Undo.UndoableObject(
+ self.do_apply_tracker,
+ self.undo_apply_tracker,
+ self.main_ui.history
+ )
+ applyer.perform()
+
+ def do_apply_tracker(self):
+ '''Apply the showing tracker to untracked
+
+ All of the values and notes will be transferred to untracked and
+ the tracker is deleted.
+ '''
+ track_row = self.find_tracker(self.tinfo.showing_tracker)
+ if not track_row:
+ return
+ # Delete the tracker
+ cleared_values, cleared_notes = self.do_delete_tracker(True)
+ # Apply the values
+ for x, y, val, tid in cleared_values:
+ self.main_ui.gsd.set_value(x, y, val)
+ # Then apply the notes
+ self.main_ui.gsd.apply_notelist(cleared_notes, True)
+ # Store the undo counts
+ self.main_ui.cleared.append(len(cleared_values))
+ self.main_ui.cleared_notes.append(len(cleared_notes))
+
+ def undo_apply_tracker(self):
+ '''Undo a previous tracker apply
+
+ The number of cleared values and notes are stored during the apply.
+ The undo is called for each of them, then the tracker delete is
+ undone.
+ '''
+ # Undo all of the applied values and notes
+ value_count = self.main_ui.cleared.pop()
+ note_count = self.main_ui.cleared_notes.pop()
+ count = 0
+ while count < (value_count + note_count):
+ self.main_ui.history.undo()
+ count += 1
+ # Undo the tracker delete
+ self.undo_delete_tracker()
+
+ def do_delete_tracker(self, for_apply = False):
+ '''Delete the current tracker
+ '''
+ track_row = self.find_tracker(self.tinfo.showing_tracker)
+ if not track_row:
+ return
+ ui_row = [track_row[0], track_row[1], track_row[2]]
+ # For the values, store it like (tracker_id, list_of_cleared_values)
+ cleared_values = self.main_ui.gsd.delete_by_tracker()
+ self.main_ui.cleared.append((self.tinfo.showing_tracker, ui_row, cleared_values))
+ # The notes already have tracker info in them, so just store the list
+ cleared_notes = self.main_ui.gsd.clear_notes(tracker = self.tinfo.showing_tracker)
+ self.main_ui.cleared_notes.append(cleared_notes)
+ # Delete it from tracker_info
+ self.hide_tracker_cb(None)
+ self.tracker_model.remove(track_row.iter)
+ self.tinfo.delete_tracker(ui_row[0])
+ # Return all of the data for "Apply Tracker" button
+ if for_apply:
+ return (cleared_values, cleared_notes)
+
+ def undo_delete_tracker(self):
+ '''Undo a tracker delete
+ '''
+ # Values are stored like (tracker_id, list_of_cleared_values)
+ tracker_id, ui_row, cleared_values = self.main_ui.cleared.pop()
+ # Recreate it in tracker_info
+ self.tinfo.create_tracker(tracker_id)
+ # Add it to the tree
+ self.tracker_tree.get_selection().select_iter(self.tracker_model.append(ui_row))
+ # Add all the values
+ for value in cleared_values:
+ self.main_ui.gsd.add_value(*value)
+ # The notes already have tracker info in them, so just store the list
+ self.main_ui.gsd.apply_notelist(self.main_ui.cleared_notes.pop())
def start_game ():
if options.debug:
diff --git a/gnome-sudoku/src/lib/number_box.py b/gnome-sudoku/src/lib/number_box.py
index 1e5db11..dca23e6 100644
--- a/gnome-sudoku/src/lib/number_box.py
+++ b/gnome-sudoku/src/lib/number_box.py
@@ -3,7 +3,7 @@
import gtk, gobject, pango, cairo
import math
-import timer
+import tracker_info
from gettext import gettext as _
ERROR_HIGHLIGHT_COLOR = (1.0, 0, 0)
@@ -79,10 +79,12 @@ class NumberBox (gtk.Widget):
_bottom_note_layout = None
text_color = None
highlight_color = None
+ shadow_color = None
custom_background_color = None
__gsignals__ = {
'value-about-to-change':(gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+ 'notes-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
@@ -104,6 +106,13 @@ class NumberBox (gtk.Widget):
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.top_note_list = []
+ self.bottom_note_list = []
+ self.tinfo = tracker_info.TrackerInfo()
self.set_property('can-focus', True)
self.set_property('events', gtk.gdk.ALL_EVENTS_MASK)
self.connect('button-press-event', self.button_press_cb)
@@ -200,18 +209,12 @@ class NumberBox (gtk.Widget):
self.add_note_text(txt, top = True)
elif e.state & gtk.gdk.MOD1_MASK:
self.remove_note_text(txt, top = True)
- elif self.get_text() != txt:
- # If there's no change, do nothing
-
- # 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
+ 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('')
@@ -300,7 +303,6 @@ class NumberBox (gtk.Widget):
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))
@@ -339,9 +341,8 @@ class NumberBox (gtk.Widget):
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._top_note_layout.set_font_description(font)
+ self._bottom_note_layout.set_font_description(font)
self.queue_draw()
def set_text (self, text):
@@ -349,31 +350,159 @@ class NumberBox (gtk.Widget):
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])
+ 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])
+ 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])
self.queue_draw()
- def set_note_text (self, top_text = None, bottom_text = None):
+ def set_note_text (self, top_text = None, bottom_text = None, for_hint = False):
+ '''Change the notes
+ '''
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)
+ self.update_notelist(self.top_note_list, top_text)
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()
+ 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('value-about-to-change')
+ 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 += '</span>'
+ if self.tinfo.get_color_markup(tid):
+ markup_text += '<span foreground="' + str(self.tinfo.get_color_markup(tid)) + '">'
+ last_tracker = tid
+ markup_text += note
+ if self.tinfo.get_color_markup(last_tracker):
+ markup_text += '</span>'
+ 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()
+
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
@@ -517,6 +646,14 @@ class NumberBox (gtk.Widget):
cr.stroke()
def draw_text (self, cr):
+ 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)
+ cr.show_layout(self._layout)
if self.text_color:
cr.set_source_rgb(*self.text_color)
elif self.read_only:
@@ -525,7 +662,6 @@ class NumberBox (gtk.Widget):
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),
@@ -551,7 +687,8 @@ class NumberBox (gtk.Widget):
cr.update_layout(self._bottom_note_layout)
cr.show_layout(self._bottom_note_layout)
- def set_text_color (self, color):
+ def set_text_color (self, color, shadow = None):
+ self.shadow_color = shadow
self.text_color = color
self.queue_draw()
@@ -565,10 +702,6 @@ class NumberBox (gtk.Widget):
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))
@@ -591,19 +724,41 @@ class NumberBox (gtk.Widget):
class SudokuNumberBox (NumberBox):
normal_color = None
+ tracker_id = None
error_color = (1.0, 0, 0)
highlight_color = ERROR_HIGHLIGHT_COLOR
- def set_color (self, color):
- self.normal_color = 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 unset_color (self):
- self.set_color(None)
+ 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:
- self.set_text_color(self.error_color)
+ 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)
diff --git a/gnome-sudoku/src/lib/saver.py b/gnome-sudoku/src/lib/saver.py
index 06bd441..357bf81 100644
--- a/gnome-sudoku/src/lib/saver.py
+++ b/gnome-sudoku/src/lib/saver.py
@@ -3,6 +3,7 @@ import gtk, pickle, types, os, errno
import defaults
from gtk_goodies.dialog_extras import show_message
from gettext import gettext as _
+import tracker_info
SAVE_ATTRIBUTES = [('gsd.hints'),
('gsd.impossible_hints'),
@@ -48,13 +49,11 @@ def jar_game (ui):
jar = {} # what we will pickle
ui.timer.mark_timing()
jar['game'] = ui.gsd.grid.to_string()
- jar['trackers'] = ui.gsd.trackers
- jar['tracking'] = ui.gsd.__trackers_tracking__
- jar['notes'] = []
+ jar['tracker_info'] = tracker_info.TrackerInfo().save()
+ jar['tracked_notes'] = []
for e in ui.gsd.__entries__.values():
- top, bot = e.get_note_text()
- if top or bot:
- jar['notes'].append((e.x, e.y, top, bot))
+ if e.top_note_list or e.bottom_note_list:
+ jar['tracked_notes'].append((e.x, e.y, e.top_note_list, e.bottom_note_list))
for attr in SAVE_ATTRIBUTES:
jar[attr] = super_getattr(ui, attr)
return jar
@@ -64,22 +63,39 @@ def set_value_from_jar (dest, jar):
super_setattr(dest, attr, jar[attr])
def open_game (ui, jar):
+ tinfo = tracker_info.TrackerInfo()
+ tinfo.set_tracker(tracker_info.NO_TRACKER)
ui.gsd.load_game(jar['game'])
- # this is a bit easily breakable... we take advantage of the fact
- # that we create tracker IDs sequentially and that {}.items()
- # sorts by keys by default
- for tracker, tracked in jar.get('trackers', {}).items():
- # add 1 tracker per existing tracker...
- ui.tracker_ui.add_tracker()
- for x, y, val in tracked:
- ui.gsd.add_tracker(x, y, tracker, val = val)
- for tracker, tracking in jar.get('tracking', {}).items():
- if tracking:
- ui.tracker_ui.select_tracker(tracker)
- set_value_from_jar(ui, jar)
+ # The 'notes' and 'trackers' sections are for transition from the old
+ # style tracker storage. The tracker values and notes are stored in the
+ # 'tracked_notes' and 'tracker_info' sections now.
if jar.has_key('notes') and jar['notes']:
for x, y, top, bot in jar['notes']:
ui.gsd.__entries__[(x, y)].set_note_text(top, bot)
+ if jar.has_key('trackers'):
+ for tracker, tracked in jar.get('trackers', {}).items():
+ # add 1 tracker per existing tracker...
+ ui.tracker_ui.add_tracker()
+ for x, y, val in tracked:
+ tinfo.add_trace(x, y, val)
+ set_value_from_jar(ui, jar)
+ if jar.has_key('tracking'):
+ for tracker, tracking in jar.get('tracking', {}).items():
+ if tracking:
+ ui.tracker_ui.select_tracker(tracker)
+ if jar.has_key('tracked_notes') and jar['tracked_notes']:
+ for x, y, top, bot in jar['tracked_notes']:
+ ui.gsd.__entries__[(x, y)].set_notelist(top, bot)
+ if jar.has_key('tracker_info'):
+ trackers = jar['tracker_info'][2]
+ for tracking in trackers.keys():
+ ui.tracker_ui.add_tracker(tracker_id = tracking)
+ tinfo.load(jar['tracker_info'])
+ ui.tracker_ui.select_tracker(tinfo.current_tracker)
+ if tinfo.showing_tracker != tracker_info.NO_TRACKER:
+ ui.gsd.show_track()
+ # Display the notes
+ ui.gsd.update_all_notes()
def pickle_game (ui, target):
close_me = False
diff --git a/gnome-sudoku/src/lib/tracker_info.py b/gnome-sudoku/src/lib/tracker_info.py
new file mode 100644
index 0000000..798c4ac
--- /dev/null
+++ b/gnome-sudoku/src/lib/tracker_info.py
@@ -0,0 +1,208 @@
+# -*- coding: utf-8 -*-
+#!/usr/bin/python
+
+import random
+import copy
+
+NO_TRACKER = -1 # Tracker id for untracked values
+
+class TrackerInfo(object):
+ '''Tracker state machine(singleton)
+
+ The singleton instance of this class is used to manipulate tracker
+ selection and tracked values, as well as interrogate tracker colors.
+
+ _tracks - dictionary for tracked values. The tracker id is used as the
+ key. A tracker is a dictionary that stored tracked values keyed by
+ its coordinates(x, y). _tracks[tracker_id][(x, y)] == tracked value
+
+ current_tracker - The tracker id for the currently selected tracker
+ showing_tracker - The tracker id for the tracker that is currently being
+ viewed. The point to this member is to store off the tracker when
+ the player switches to "Untracked" so that the last tracker they were
+ working on stays in view after the switch.
+ '''
+ __single = None
+ _tracks = {}
+ _colors = {}
+ current_tracker = NO_TRACKER
+ showing_tracker = NO_TRACKER
+
+ def __new__(cls, *args, **kwargs):
+ '''Overridden to implement as a singleton
+ '''
+ # Check to see if a __single exists already for this class
+ # Compare class types instead of just looking for None so
+ # that subclasses will create their own __single objects
+ if cls != type(cls.__single):
+ cls.__single = object.__new__(cls, *args, **kwargs)
+ return cls.__single
+
+ def __init__(self):
+ # Only initialize the colors once
+ if self._colors:
+ return
+ # Use tango colors recommended here:
+ # http://tango.freedesktop.org/Tango_Icon_Theme_Guidelines
+ for tracker_id, cols in enumerate(
+ [(32, 74, 135), # Sky Blue 3
+ (78, 154, 6), # Chameleon 3
+ (206, 92, 0), # Orange 3
+ (143, 89, 2), # Chocolate 3
+ (92, 53, 102), # Plum 3
+ (85, 87, 83), # Aluminium 5
+ (196, 160, 0) # Butter 3
+ ]):
+ self._colors[tracker_id] = tuple([x / 255.0 for x in cols])
+
+ def load(self, pickle):
+ self.current_tracker, self.showing_tracker, self._tracks = pickle
+
+ def save(self):
+ return (self.current_tracker, self.showing_tracker, self.get_trackers())
+
+ def create_tracker (self, tracker_id = 0):
+ '''Create storage for a new tracker
+
+ tracker_id can be passed in to attempt creation of a specific id, but
+ if the tracker_id already exists then the passed number will be
+ incremented until a suitable key can be allocated.
+ '''
+ if not tracker_id:
+ tracker_id = 0
+ while self._tracks.has_key(tracker_id):
+ tracker_id += 1
+ self._tracks[tracker_id] = {}
+ return tracker_id
+
+ def get_tracker(self, tracker_id):
+ if self._tracks.has_key(tracker_id):
+ return self._tracks[tracker_id]
+
+ def delete_tracker(self, tracker_id):
+ if self._tracks.has_key(tracker_id):
+ del self._tracks[tracker_id]
+
+ def reset (self):
+ ''' Reset the tracker information
+ '''
+ self._tracks = {}
+ self.current_tracker = NO_TRACKER
+ self.showing_tracker = NO_TRACKER
+
+ def use_trackers (self, trackers):
+ self._tracks = trackers
+
+ def get_trackers(self):
+ return copy.deepcopy(self._tracks)
+
+ def set_tracker(self, tracker_id):
+ self.current_tracker = tracker_id
+ if tracker_id != NO_TRACKER:
+ self.showing_tracker = tracker_id
+
+ def hide_tracker(self):
+ self.showing_tracker = NO_TRACKER
+
+ def get_tracker_view(self):
+ return((self.current_tracker, self.showing_tracker))
+
+ def set_tracker_view(self, tview):
+ self.current_tracker, self.showing_tracker = tview
+
+ def get_color (self, tracker_id):
+ # Untracked items don't get specially colored
+ if tracker_id == NO_TRACKER:
+ return None
+ # Create a random color for new trackers that are beyond the defaults
+ if not self._colors.has_key(tracker_id):
+ random_color = self._colors[0]
+ while random_color in self._colors.values():
+ # If we have generated all possible colors, this will
+ # enter an infinite loop
+ random_color = (random.randint(0, 100)/100.0,
+ random.randint(0, 100)/100.0,
+ random.randint(0, 100)/100.0)
+ self._colors[tracker_id] = random_color
+ return self._colors[tracker_id]
+
+ def get_color_markup(self, tracker_id):
+ color_tuple = self.get_color (tracker_id)
+ if not color_tuple:
+ return None
+ color_markup = '#'
+ color_markup += str(hex(int(color_tuple[0]*255))[2:]).zfill(2)
+ color_markup += str(hex(int(color_tuple[1]*255))[2:]).zfill(2)
+ color_markup += str(hex(int(color_tuple[2]*255))[2:]).zfill(2)
+ return color_markup.upper()
+
+ def get_current_color(self):
+ return self.get_color(self.current_tracker)
+
+ def get_showing_color(self):
+ return self.get_color(self.showing_tracker)
+
+ def add_trace(self, x, y, value, tracker_id = None):
+ '''Add a tracked value
+
+ By default(tracker_id set to None) this method adds a value to the
+ current tracker. tracker_id can be passed in to add it to a specific
+ tracker.
+ '''
+ if tracker_id == None:
+ to_tracker = self.current_tracker
+ else:
+ to_tracker = tracker_id
+ # Need a tracker
+ if to_tracker == NO_TRACKER:
+ return
+ # Make sure the dictionary is available for the tracker.
+ if not self._tracks.has_key(to_tracker):
+ self._tracks[to_tracker] = {}
+ # Add it
+ self._tracks[to_tracker][(x, y)] = value
+
+ def remove_trace(self, x, y, from_tracker = None):
+ '''Remove a tracked value
+
+ By default(from_tracker set to None) this method removes all tracked
+ values for a particular cell(x, y coords). from_tracker can be passed
+ to remove tracked values from a particular tracker only.
+ '''
+ if from_tracker == None:
+ from_tracks = self._tracks.keys()
+ else:
+ from_tracks = [from_tracker]
+ # Delete them
+ for tracker in from_tracks:
+ if self._tracks.has_key(tracker) and self._tracks[tracker].has_key((x, y)):
+ del self._tracks[tracker][(x, y)]
+
+ def get_trackers_for_cell(self, x, y):
+ '''Return all trackers for a cell
+
+ This function is used for the undo mechanism. A list in the format
+ (tracker, value) is returned so that it may later be reinstated with
+ reset_trackers_for_cell().
+ '''
+ ret = []
+ for tracker, track in self._tracks.items():
+ if track.has_key((x, y)):
+ ret.append((tracker, track[(x, y)]))
+ return ret
+
+ def reset_trackers_for_cell(self, x, y, old_trackers):
+ '''Reset all trackers to a previous state for a cell
+
+ This function is used for the undo mechanism. It reinstates the
+ tracked values the list created by get_trackers_for_cell().
+ '''
+ # Remove all the current traces
+ for tracker, track in self._tracks.items():
+ if track.has_key((x, y)):
+ del self._tracks[tracker][(x, y)]
+ # Add the old ones back
+ for tracker, value in old_trackers:
+ self._tracks[tracker][(x, y)] = value
+
+
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]