Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJoe Lee <joe@jotaro.com>2009-08-27 02:57:17 (GMT)
committer Joe Lee <joe@jotaro.com>2009-08-27 02:57:17 (GMT)
commit9da62da7920d719f88f8c967a42294670cb723ea (patch)
tree56a2ab265e84b89013abae0dfc8758d2a61925f1
parent5cb9dc3bff3298f763e4b3d6c5a77393c515462f (diff)
Implemented help window.
-rw-r--r--MANIFEST2
-rw-r--r--gridwidget.py8
-rw-r--r--helpwidget.py891
-rw-r--r--icons/help-icon.svg14
-rw-r--r--implodeactivity.py173
-rw-r--r--sugarless.py105
6 files changed, 1186 insertions, 7 deletions
diff --git a/MANIFEST b/MANIFEST
index cd67198..8f7a8ef 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -4,6 +4,7 @@ board.py
boardgen.py
color.py
gridwidget.py
+helpwidget.py
icons/easy-level.svg
icons/edit-redo.svg
icons/edit-undo.svg
@@ -11,6 +12,7 @@ icons/hard-level.svg
icons/medium-level.svg
icons/new-game.svg
icons/replay-game.svg
+icons/help.svg
implodeactivity.py
implodegame.py
po/Implode.pot
diff --git a/gridwidget.py b/gridwidget.py
index 3cfe057..2bf78a8 100644
--- a/gridwidget.py
+++ b/gridwidget.py
@@ -441,6 +441,12 @@ class BoardDrawer(object):
self._invalidate_selection(old_selection)
self._invalidate_selection(self._selected_cell)
+ def get_block_coord(self, x, y):
+ if not self.board_is_valid():
+ return (0, 0)
+ (block_x, block_y) = self._cell_to_display(x + 0.5, y + 0.5)
+ return (block_x, block_y)
+
def _invalidate_board(self):
(width, height) = self._get_size_func()
rect = gtk.gdk.Rectangle(0, 0, width, height)
@@ -1053,6 +1059,8 @@ class _BoardTransform(object):
return (x1, y1)
def inverse_transform(self, x, y):
+ if self.scale_x == 0 or self.scale_y == 0:
+ return (0, 0)
x1 = int((float(x) - self.offset_x) / self.scale_x)
y1 = int((float(y) - self.offset_y) / self.scale_y)
return (x1, y1)
diff --git a/helpwidget.py b/helpwidget.py
new file mode 100644
index 0000000..d0dd7da
--- /dev/null
+++ b/helpwidget.py
@@ -0,0 +1,891 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2009, Joseph C. Lee
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+from __future__ import with_statement
+
+from gettext import gettext as _
+
+import cairo
+import gobject
+import gtk
+import math
+import os
+import rsvg
+import time
+
+import board
+from gridwidget import Anim, BoardDrawer, RemovalDrawer, WinDrawer
+
+if 'SUGAR_BUNDLE_PATH' in os.environ:
+ from sugar.graphics import style
+ _DEFAULT_SPACING = style.DEFAULT_SPACING
+ _DEFAULT_PADDING = style.DEFAULT_PADDING
+ _BG_COLOR = tuple(style.COLOR_PANEL_GREY.get_rgba()[:3])
+ _TOOLBAR_COLOR = tuple(style.COLOR_TOOLBAR_GREY.get_rgba()[:3])
+else:
+ # Fallbacks for non-Sugar testing.
+ _DEFAULT_SPACING = 15
+ _DEFAULT_PADDING = 6
+ _BG_COLOR = (0.75, 0.75, 0.75)
+ _TOOLBAR_COLOR = (0.16, 0.16, 0.16)
+
+_CURSOR_COLOR = (.8, .8, .8)
+_CURSOR_OUTLINE_COLOR = (.4, .4, .4)
+
+# Proportion of the _PreviewWidget's height occupied by emulated button bar.
+_ICON_HEIGHT = 0.1
+
+# Proportion of the _PreviewWidget's height to scale the mouse cursor.
+_CURSOR_SCALE = 0.12
+
+# Proportion of the _PreviewWidget's cursor width to make the thickness of its
+# lines.
+_CURSOR_WEIGHT_SCALE = 0.15
+_CURSOR_OUTLINE_WEIGHT_SCALE = 0.3
+
+# Proportion of the _PreviewWidget's cursor width to make the mouse click
+# animation.
+_CLICK_INNER_RADIUS = 0.1
+_CLICK_OUTER_RADIUS = 0.7
+
+# Proportion of the _PreviewWidget's cursor width to make the thickness of the
+# click animation's lines.
+_CLICK_WEIGHT_SCALE = 0.1
+_CLICK_OUTLINE_WEIGHT_SCALE = 0.2
+
+# Speed of the click animation, in seconds.
+_CLICK_SPEED = 0.2
+
+# Speed of the mouse, in units (4x3 per screen) per second.
+_MOUSE_SPEED = 0.5
+
+class HelpWidget(gtk.EventBox):
+ def __init__(self, icon_file_func, *args, **kwargs):
+ super(HelpWidget, self).__init__(*args, **kwargs)
+
+ vbox = gtk.VBox()
+ self.add(vbox)
+
+ self._stages = [
+ _HelpStage1(icon_file_func),
+ _HelpStage2(icon_file_func),
+ _HelpStage3(icon_file_func),
+ _HelpStage4(icon_file_func),
+ _HelpStage5(icon_file_func),
+ ]
+ self._stage_index = 0
+ self._notebook = gtk.Notebook()
+ self._notebook.set_show_tabs(False)
+ for stage in self._stages:
+ self._notebook.append_page(stage)
+ vbox.pack_start(self._notebook)
+
+ self._reset_current_stage()
+
+ def can_prev_stage(self):
+ """Returns True if the help widget can move to the previous stage."""
+ return (self._stage_index != 0)
+
+ def can_next_stage(self):
+ """Returns True if the help widget can move to the next stage."""
+ return (self._stage_index < len(self._stages) - 1)
+
+ def prev_stage(self):
+ """Moves the help widget to the previous stage."""
+ self._stage_index = max(0, self._stage_index - 1)
+ self._reset_current_stage()
+
+ def next_stage(self):
+ """Moves the help widget to the next stage."""
+ self._stage_index = min(len(self._stages) - 1, self._stage_index + 1)
+ self._reset_current_stage()
+
+ def replay_stage(self):
+ """Replays the current stage."""
+ self._stages[self._stage_index].reset()
+
+ def _reload_clicked_cb(self, source):
+ self._reset_current_stage()
+
+ def _reset_current_stage(self):
+ self._notebook.set_current_page(self._stage_index)
+ self._stages[self._stage_index].reset()
+
+
+class _HelpStage(gtk.EventBox):
+ # An abstract parent class for objects that represent an animated help
+ # screen widget with a description.
+ def __init__(self, icon_file_func, *args, **kwargs):
+ super(_HelpStage, self).__init__(*args, **kwargs)
+
+ hbox = gtk.HBox()
+ self.add(hbox)
+
+ vbox = gtk.VBox()
+ hbox.pack_start(vbox, expand=True, padding=_DEFAULT_SPACING)
+
+ self.preview = _PreviewWidget(icon_file_func)
+ vbox.pack_start(self.preview, expand=True, padding=_DEFAULT_PADDING)
+
+ label = gtk.Label(self.get_message())
+ label.set_line_wrap(True)
+ vbox.pack_start(label, expand=False, padding=_DEFAULT_PADDING)
+
+ self.board = None
+ self.undo_stack = []
+
+ self.anim = None
+ self._actions = []
+ self._action_index = 0
+
+ actions = self._get_actions()
+ self._actions = _flatten(actions)
+
+ def get_message(self):
+ # Implement to return stage message.
+ raise Exception()
+
+ def reset(self):
+ # Resets the playback of the animation script.
+ self._stop_animation()
+ self._action_index = 0
+ self.preview.set_cursor_visible(True)
+ self.preview.set_click_visible(False)
+ self.preview.center_cursor()
+ self.next_action()
+
+ def set_board(self, board):
+ self.board = board.clone()
+ self.preview.board_drawer.set_board(self.board)
+
+ def _stop_animation(self):
+ if self.anim:
+ self.anim.stop()
+ self.anim = None
+
+ def next_action(self):
+ # Moves the HelpStage animation script to the next action.
+ if self._action_index >= len(self._actions):
+ self.preview.set_cursor_visible(False)
+ return
+ action = self._actions[self._action_index]
+ self._action_index += 1
+ action(self)
+
+ def _get_actions(self):
+ # Implement to return a list stage actions (optionally containing
+ # sublists of actions).
+ raise Exception()
+
+
+class _HelpStage1(_HelpStage):
+ def __init__(self, *args, **kwargs):
+ super(_HelpStage1, self).__init__(*args, **kwargs)
+
+ def get_message(self):
+ return _("Goal: Clear the board by removing blocks in groups of 3 or more.")
+
+ def _get_actions(self):
+ return [
+ _set_board("""..33.
+ .2231
+ 12231"""),
+ _pause(1),
+ _move_to_block(0, 0),
+ _pause(0.5),
+ _move_to_block(4, 0),
+ _pause(0.5),
+ _click_to_remove(1, 1),
+ _click_to_remove(2, 2),
+ _click_to_remove(1, 0, pause=0),
+ _show_win(1),
+ _pause(1),
+ ]
+
+
+class _HelpStage2(_HelpStage):
+ def __init__(self, *args, **kwargs):
+ super(_HelpStage2, self).__init__(*args, **kwargs)
+
+ def get_message(self):
+ return _("You can't remove groups of one or two blocks.")
+
+ def _get_actions(self):
+ return [
+ _set_board(""".1..
+ .221
+ 1121"""),
+ _pause(1),
+ _move_to_block(0, 0),
+ _pause(1),
+ _click(),
+ _pause(1),
+ _move_to_block(0, 1),
+ _move_to_block(1, 2),
+ _pause(1),
+ _click(),
+ _pause(1),
+ _move_to_block(2, 2),
+ _move_to_block(3, 1),
+ _move_to_block(3, 0),
+ _pause(1),
+ _click(),
+ _pause(1),
+ _move_to_block(3, 1),
+ _move_to_block(2, 2),
+ _move_to_block(1, 2),
+ _pause(0.5),
+ _move_to_block(0, 1),
+ _move_to_block(0, 0),
+ _move_to_block(1, 0),
+ _pause(1),
+ _click_to_remove(2, 1),
+ _click_to_remove(0, 0, pause=0),
+ _show_win(2),
+ _pause(1),
+ ]
+
+
+class _HelpStage3(_HelpStage):
+ def __init__(self, *args, **kwargs):
+ super(_HelpStage3, self).__init__(*args, **kwargs)
+
+ def get_message(self):
+ return _("Blocks fall to fill empty gaps, and they slide to fill empty columns.")
+
+ def _get_actions(self):
+ return [
+ _set_board(""".333.
+ 1222.
+ 12221
+ 32223"""),
+ _pause(2),
+ _click_to_remove(2, 1),
+ _pause(1),
+ _click_to_remove(1, 0),
+ _click_to_remove(0, 1, pause=0),
+ _show_win(3),
+ _pause(1),
+ ]
+
+
+class _HelpStage4(_HelpStage):
+ def __init__(self, *args, **kwargs):
+ super(_HelpStage4, self).__init__(*args, **kwargs)
+
+ def get_message(self):
+ return _("If you get stuck, you can undo to try again.")
+
+ def _get_actions(self):
+ return [
+ _set_board("""1211
+ 1221"""),
+ _pause(2),
+ _click_to_remove(2, 1),
+ _click_to_remove(1, 1),
+ _move_to_block(0, 1),
+ _pause(1),
+ _click(),
+ _pause(1),
+ _click(),
+ _pause(2),
+ _click_to_undo(),
+ _click_to_undo(),
+ _click_to_remove(1, 1),
+ _click_to_remove(0, 0, pause=0),
+ _show_win(4),
+ _pause(1),
+ ]
+
+def _click_to_remove(x, y, pause=2):
+ # Returns an array of action functions to remove the block at (x, y).
+ return [
+ _move_to_block(x, y),
+ _pause(1),
+ _click(),
+ _remove_piece(x, y),
+ _pause(pause),
+ ]
+
+def _click_to_undo():
+ # Returns an array of action functions to undo the last move.
+ return [
+ _move_to_icon(2),
+ _pause(1),
+ _click(),
+ _undo(),
+ ]
+
+
+class _HelpStage5(_HelpStage):
+ def __init__(self, *args, **kwargs):
+ super(_HelpStage5, self).__init__(*args, **kwargs)
+
+ def get_message(self):
+ return _("There is always a way to clear the board.")
+
+ def _get_actions(self):
+ return [
+ # Difficult game seed: 5234
+ _set_board("""132.1..1.1...4
+ 15244.25.1...4
+ 12244114.1..44
+ 12254314.1..43
+ 15251324.1..11
+ 15253413.1..14
+ 53251213.5..43
+ 53252113.5..33
+ 53242114.5..33
+ 34232114.1..32
+ 34115113.51111
+ 54131215452221
+ 24231423423222
+ 24245323423224
+ 24245423423244"""),
+ _click_to_remove(12, 9),
+ _click_to_remove(12, 9),
+ _click_to_remove(2, 12),
+ _click_to_remove(1, 11),
+ _click_to_remove(3, 4),
+ _click_to_remove(3, 3),
+ _click_to_remove(3, 2),
+ _click_to_remove(1, 3),
+ _click_to_remove(1, 3),
+ _click_to_remove(1, 2),
+ _click_to_remove(5, 9),
+ _click_to_remove(2, 6),
+ _click_to_remove(3, 6),
+ _click_to_undo(),
+ _click_to_undo(),
+ _click_to_remove(3, 7),
+ _click_to_remove(2, 7),
+ _click_to_remove(3, 6),
+ _click_to_remove(6, 1),
+ _click_to_remove(6, 4),
+ _click_to_remove(5, 4),
+ _click_to_remove(6, 4),
+ _click_to_remove(6, 3),
+ _click_to_remove(2, 5),
+ _click_to_remove(3, 4),
+ _click_to_remove(4, 3),
+ _click_to_remove(1, 4),
+ _click_to_undo(),
+ _click_to_remove(2, 5),
+ _click_to_remove(1, 4),
+ _click_to_remove(1, 2),
+ _click_to_remove(1, 2),
+ _click_to_remove(1, 1),
+ _click_to_remove(1, 1),
+ _click_to_remove(1, 1),
+ _click_to_remove(1, 1),
+ _click_to_remove(1, 0),
+ _click_to_remove(0, 0, pause=0),
+ _show_win(5),
+ _pause(1),
+ ]
+
+# The following are functions that return a function that, given a HelpStage
+# object will set it up to perform the appropriate action.
+
+def _set_board(board_string):
+ # Returns a function to reset the game board to a given state.
+ board = _make_board(board_string)
+ def action(stage):
+ stage.set_board(board)
+ stage.undo_stack = []
+ stage.preview.set_drawer(stage.preview.board_drawer)
+ stage.next_action()
+ return action
+
+def _pause(delay):
+ # Returns a function to delay playback by the given amount of time.
+ def action(stage):
+ start_time = time.time()
+ def update_func():
+ delta = time.time() - start_time
+ return delta < delay
+ def end_anim_func(anim_stopped):
+ if not anim_stopped:
+ stage.next_action()
+ stage.anim = Anim(update_func, end_anim_func)
+ stage.anim.start()
+ return action
+
+def _move_to_block(x, y):
+ # Returns a function to move the mouse cursor to the given block coordinate.
+ def coord_func(stage):
+ return stage.preview.get_block_coord(x, y)
+ return _move_to(coord_func)
+
+def _move_to_icon(index):
+ # Returns a function to move the mouse cursor to the given icon.
+ def coord_func(stage):
+ return stage.preview.get_icon_coord(index)
+ return _move_to(coord_func)
+
+def _move_to(coord_func):
+ def action(stage):
+ # Caveat: This has the potential to get a little messed up if it is an
+ # early action in a stage or if the screen changes size as the cursor
+ # is moving... Best to keep a pause before it in the sequence.
+ (old_x, old_y) = stage.preview.get_cursor_pos()
+ (new_x, new_y) = coord_func(stage)
+ delta_x = new_x - old_x
+ delta_y = new_y - old_y
+ dist = math.sqrt(delta_x * delta_x + delta_y * delta_y)
+ move_time = dist * _MOUSE_SPEED
+ start_time = time.time()
+ def update_func():
+ delta = time.time() - start_time
+ if delta >= move_time or move_time == 0.0:
+ return False
+ t = max(0.0, min(1.0, delta / move_time))
+ # Use the first half of cosine wave to ease in/out.
+ w = 1.0 - (0.5 * math.cos(t * math.pi) + 0.5)
+ inv_w = 1.0 - w
+ move_x = old_x * inv_w + new_x * w
+ move_y = old_y * inv_w + new_y * w
+ stage.preview.set_cursor_pos(move_x, move_y)
+ return True
+ def end_anim_func(anim_stopped):
+ if not anim_stopped:
+ stage.next_action()
+ stage.anim = Anim(update_func, end_anim_func)
+ stage.anim.start()
+ return action
+
+def _click():
+ # Returns a function to play the mouse-click animation.
+ def action(stage):
+ start_time = time.time()
+ stage.preview.set_click_visible(True)
+ def update_func():
+ delta = time.time() - start_time
+ return (delta < _CLICK_SPEED)
+ def end_anim_func(anim_stopped):
+ stage.preview.set_click_visible(False)
+ if not anim_stopped:
+ stage.next_action()
+ stage.anim = Anim(update_func, end_anim_func)
+ stage.anim.start()
+ return action
+
+def _remove_piece(x, y):
+ # Returns a function to animate the removal of the given piece.
+ def action(stage):
+ contiguous = stage.board.get_contiguous(x, y)
+ removal_drawer = stage.preview.removal_drawer
+ stage.preview.set_drawer(removal_drawer)
+ removal_drawer.init(stage.board, contiguous)
+ removal_drawer.set_anim_time(0.0)
+ start_time = time.time()
+
+ def update_func(start_time_ref=[start_time]):
+ delta = time.time() - start_time_ref[0]
+ length = removal_drawer.get_anim_length()
+ if delta > length:
+ if not removal_drawer.next_stage():
+ return False
+ start_time_ref[0] = time.time()
+ delta = 0.0
+ removal_drawer.set_anim_time(delta)
+ return True
+
+ def local_end_anim_func(anim_stopped):
+ stage.preview.set_drawer(stage.preview.board_drawer)
+ stage.undo_stack.append(stage.board)
+ board = stage.board.clone()
+ board.clear_pieces(contiguous)
+ board.drop_pieces()
+ board.remove_empty_columns()
+ stage.set_board(board)
+ if not anim_stopped:
+ stage.next_action()
+ stage.anim = Anim(update_func, local_end_anim_func)
+ stage.anim.start()
+ return action
+
+def _show_win(color):
+ # Returns a function to animate a win.
+ def action(stage):
+ win_drawer = stage.preview.win_drawer
+ stage.preview.set_drawer(win_drawer)
+ win_drawer.set_win_state(True, color)
+ length = win_drawer.get_anim_length()
+ start_time = time.time()
+
+ def update_func():
+ delta = time.time() - start_time
+ win_drawer.set_anim_time(min(delta, length))
+ return (delta <= length)
+
+ def local_end_anim_func(anim_stopped):
+ win_drawer.set_anim_time(length)
+ if not anim_stopped:
+ stage.next_action()
+
+ stage.anim = Anim(update_func, local_end_anim_func)
+ stage.anim.start()
+ return action
+
+def _undo():
+ # Returns a function that undoes the previous move.
+ def action(stage):
+ board = stage.undo_stack.pop()
+ stage.set_board(board)
+ stage.next_action()
+ return action
+
+class _PreviewWidget(gtk.DrawingArea):
+ __gsignals__ = {
+ 'expose-event': 'override',
+ 'size-allocate': 'override',
+ }
+
+ def __init__(self, icon_file_func, *args, **kwargs):
+ super(_PreviewWidget, self).__init__(*args, **kwargs)
+
+ self.board_drawer = \
+ BoardDrawer(get_size_func=self._get_drawer_size,
+ invalidate_rect_func=self._invalidate_drawer_rect)
+ self.removal_drawer = \
+ RemovalDrawer(get_size_func=self._get_drawer_size,
+ invalidate_rect_func=self._invalidate_drawer_rect)
+ self.win_drawer = \
+ WinDrawer(get_size_func=self._get_drawer_size,
+ invalidate_rect_func=self._invalidate_drawer_rect)
+
+ self._icon_file_func = icon_file_func
+
+ self._preview_rect = gtk.gdk.Rectangle(0, 0, 0, 0)
+ self._toolbar_rect = gtk.gdk.Rectangle(0, 0, 0, 0)
+ self._drawer_rect = gtk.gdk.Rectangle(0, 0, 0, 0)
+
+ self._drawer = self.board_drawer
+
+ # Mouse position as a floating point value over the 4x3 unit preview
+ # area.
+ self._cursor_pos = (0.0, 0.0)
+
+ # Cursor size in pixels.
+ self._cursor_size = (0, 0)
+
+ self._click_visible = False
+ self._cursor_visible = False
+
+ def _get_drawer_size(self):
+ return (self._drawer_rect.width, self._drawer_rect.height)
+
+ def _invalidate_drawer_rect(self, rect):
+ if self.window:
+ (x, y) = (self._drawer_rect.x, self._drawer_rect.y)
+ offset_rect = gtk.gdk.Rectangle(rect.x + x,
+ rect.y + y,
+ rect.width,
+ rect.height)
+ self.window.invalidate_rect(offset_rect, True)
+
+ def set_drawer(self, drawer):
+ self._drawer = drawer
+ r = self._preview_rect
+ self._invalidate_client_rect(0, 0, r.width, r.height)
+
+ def center_cursor(self):
+ self.set_cursor_pos(2.0, 1.5)
+
+ def set_cursor_pos(self, x, y):
+ self._invalidate_cursor()
+ self._cursor_pos = (x, y)
+ self._invalidate_cursor()
+ self._update_mouse_position()
+
+ def get_cursor_pos(self):
+ return self._cursor_pos
+
+ def set_click_visible(self, click_visible):
+ self._click_visible = click_visible
+ self._invalidate_click()
+
+ def set_cursor_visible(self, cursor_visible):
+ self._cursor_visible = cursor_visible
+ self._invalidate_cursor()
+
+ def get_block_coord(self, x, y):
+ # Returns the coordinate of the given board block in terms of 4x3 units.
+ if (self._preview_rect.width == 0
+ or self._preview_rect.height == 0):
+ return (0, 0)
+ (drawer_x, drawer_y) = self.board_drawer.get_block_coord(x, y)
+ preview_x = drawer_x
+ preview_y = drawer_y + self._toolbar_rect.height
+ out_x = preview_x * 4.0 / self._preview_rect.width
+ out_y = preview_y * 3.0 / self._preview_rect.height
+ return (out_x, out_y)
+
+ def get_icon_coord(self, index):
+ # Returns the coordinate of the given icon in terms of 4x3 units.
+ icon_height = self._toolbar_rect.height
+ preview_x = icon_height * (index + 0.5)
+ preview_y = icon_height * 0.5
+ out_x = preview_x * 4.0 / self._preview_rect.width
+ out_y = preview_y * 3.0 / self._preview_rect.height
+ return (out_x, out_y)
+
+ def _get_cursor_pixel_coords(self):
+ (x, y) = self._cursor_pos
+ pixel_x = x * self._preview_rect.width / 4
+ pixel_y = y * self._preview_rect.height / 3
+ return (pixel_x, pixel_y)
+
+ def _invalidate_cursor(self):
+ (pixel_x, pixel_y) = self._get_cursor_pixel_coords()
+ self._invalidate_client_rect(pixel_x, pixel_y, *self._cursor_size)
+
+ if self._click_visible:
+ self._invalidate_click()
+
+ def _invalidate_click(self):
+ (pixel_x, pixel_y) = self._get_cursor_pixel_coords()
+ r = self._cursor_size[0] * _CLICK_OUTER_RADIUS
+ r2 = r * 2
+ self._invalidate_client_rect(pixel_x - r, pixel_y - r, r2, r2)
+
+ def _invalidate_client_rect(self, x, y, width, height):
+ if self.window:
+ rect = gtk.gdk.Rectangle(
+ int(math.floor(x)) + self._preview_rect.x,
+ int(math.floor(y)) + self._preview_rect.y,
+ int(math.ceil(width)) + 1,
+ int(math.ceil(height)) + 1)
+ self.window.invalidate_rect(rect, True)
+
+ def _update_mouse_position(self):
+ (pixel_x, pixel_y) = self._get_cursor_pixel_coords()
+ (x, y) = (pixel_x, pixel_y - self._toolbar_rect.height)
+ self.board_drawer.set_mouse_selection(x, y)
+
+ def do_expose_event(self, event):
+ cr = self.window.cairo_create()
+ cr.rectangle(event.area.x,
+ event.area.y,
+ event.area.width,
+ event.area.height)
+ cr.clip()
+ (width, height) = self.window.get_size()
+ self._draw(cr, width, height)
+
+ def _draw(self, cr, width, height):
+ cr.set_antialias(cairo.ANTIALIAS_NONE)
+ cr.set_source_rgb(*_BG_COLOR)
+ cr.rectangle(0, 0, width, height)
+ cr.fill()
+
+ cr.save()
+ cr.rectangle(self._preview_rect.x,
+ self._preview_rect.y,
+ self._preview_rect.width,
+ self._preview_rect.height)
+ cr.clip()
+
+ self._draw_toolbar(cr)
+ self._draw_grid(cr)
+ if self._click_visible:
+ self._draw_click(cr)
+ if self._cursor_visible:
+ self._draw_cursor(cr)
+
+ cr.restore()
+
+ def _draw_toolbar(self, cr):
+ cr.set_source_rgb(*_TOOLBAR_COLOR)
+ cr.rectangle(self._toolbar_rect.x,
+ self._toolbar_rect.y,
+ self._toolbar_rect.width,
+ self._toolbar_rect.height)
+ cr.fill()
+
+ icon_height = self._toolbar_rect.height
+ scale = icon_height / 55.0
+ for (i, icon_name) in enumerate(['new-game',
+ 'replay-game',
+ 'edit-undo',
+ 'edit-redo']):
+ file_path = self._icon_file_func(icon_name)
+ handle = _get_icon_handle(file_path)
+ cr.save()
+ cr.translate(self._toolbar_rect.x + i * icon_height,
+ self._toolbar_rect.y)
+ cr.scale(scale, scale)
+ handle.render_cairo(cr)
+ cr.restore()
+
+ def _draw_grid(self, cr):
+ cr.save()
+ cr.translate(self._drawer_rect.x, self._drawer_rect.y)
+ self._drawer.draw(cr, self._drawer_rect.width, self._drawer_rect.height)
+ cr.restore()
+
+ def _draw_click(self, cr):
+ width = self._cursor_size[0]
+ weight = width * _CLICK_WEIGHT_SCALE
+ outline_weight = width * _CLICK_OUTLINE_WEIGHT_SCALE
+ r1 = width * _CLICK_INNER_RADIUS + outline_weight
+ r2 = width * _CLICK_OUTER_RADIUS - outline_weight
+ (pixel_x, pixel_y) = self._get_cursor_pixel_coords()
+ x = pixel_x + self._preview_rect.x
+ y = pixel_y + self._preview_rect.y
+
+ cr.save()
+ cr.translate(x, y)
+ cr.set_line_cap(cairo.LINE_CAP_ROUND)
+ angle_inc = math.pi * 2.0 / 6
+ cr.rotate(angle_inc * 0.75)
+ for i in range(6):
+ cr.set_line_width(outline_weight)
+ cr.set_source_rgb(*_CURSOR_OUTLINE_COLOR)
+ cr.move_to(r1, 0)
+ cr.line_to(r2, 0)
+ cr.stroke()
+
+ cr.set_line_width(weight)
+ cr.set_source_rgb(*_CURSOR_COLOR)
+ cr.move_to(r1, 0)
+ cr.line_to(r2, 0)
+ cr.stroke()
+
+ cr.rotate(angle_inc)
+
+ cr.restore()
+
+ def _draw_cursor(self, cr):
+ (pixel_x, pixel_y) = self._get_cursor_pixel_coords()
+ x = pixel_x + self._preview_rect.x
+ y = pixel_y + self._preview_rect.y
+ (width, height) = self._cursor_size
+ weight = width * _CURSOR_WEIGHT_SCALE
+ outline_weight = width * _CURSOR_OUTLINE_WEIGHT_SCALE
+ hw = outline_weight / 2.0
+
+ def draw_arrow():
+ cr.move_to(x + width * 0.9 - hw, y + hw)
+ cr.line_to(x + hw, y + hw)
+ cr.line_to(x + hw, y + height * 0.9 - hw)
+ cr.move_to(x + hw, y + hw)
+ cr.line_to(x + width - hw, y + height - hw)
+ cr.stroke()
+
+ cr.save()
+ cr.set_line_cap(cairo.LINE_CAP_ROUND)
+ cr.set_line_join(cairo.LINE_JOIN_ROUND)
+
+ cr.set_line_width(outline_weight)
+ cr.set_source_rgb(*_CURSOR_OUTLINE_COLOR)
+ draw_arrow()
+
+ cr.set_line_width(weight)
+ cr.set_source_rgb(*_CURSOR_COLOR)
+ draw_arrow()
+
+ cr.restore()
+
+ def do_size_allocate(self, allocation):
+ super(_PreviewWidget, self).do_size_allocate(self, allocation)
+ (width, height) = (allocation.width, allocation.height)
+
+ avail_width = width - _DEFAULT_SPACING * 2
+ other_height = avail_width * 3 / 4
+
+ avail_height = height - _DEFAULT_SPACING * 2
+ other_width = avail_height * 4 / 3
+
+ if other_height < avail_height:
+ actual_width = avail_width
+ actual_height = other_height
+ else:
+ actual_width = other_width
+ actual_height = avail_height
+
+ icon_height = int(math.ceil(actual_height * _ICON_HEIGHT))
+ board_height = actual_height - icon_height
+
+ x_offset = (width - actual_width) / 2
+ y_offset = (height - actual_height) / 2
+
+ old_width = self._preview_rect.width
+ old_height = self._preview_rect.height
+
+ self._preview_rect = gtk.gdk.Rectangle(x_offset,
+ y_offset,
+ actual_width,
+ actual_height)
+ self._toolbar_rect = gtk.gdk.Rectangle(x_offset,
+ y_offset,
+ actual_width,
+ icon_height)
+ self._drawer_rect = gtk.gdk.Rectangle(x_offset,
+ y_offset + icon_height,
+ actual_width,
+ board_height)
+ self.board_drawer.resize(actual_width, board_height)
+ self.removal_drawer.resize(actual_width, board_height)
+ self.win_drawer.resize(actual_width, board_height)
+
+ cursor_width = actual_height * _CURSOR_SCALE
+ self._cursor_size = (cursor_width, cursor_width)
+
+ self._update_mouse_position()
+
+
+def _make_board(board_string):
+ # Given a string with numbers representing colors, periods representing
+ # spaces, and lines separated by whitespace, returns a board object.
+ b = board.Board()
+ lines = [x.strip() for x in board_string.strip().split()]
+
+ val_map = dict([('.', None)] + [(str(i), i) for i in range(1, 10)])
+
+ for (i, line) in enumerate(reversed(lines)):
+ for (j, ch) in enumerate(line):
+ b.set_value(j, i, val_map[ch])
+
+ return b
+
+def _flatten(items):
+ # Returns a flattened list of items.
+ out = []
+ for item in items:
+ if isinstance(item, list):
+ out.extend(_flatten(item))
+ else:
+ out.append(item)
+ return out
+
+# Simple caching mechanism for getting rsvg rendering handles for icons. (The
+# sugar.graphics.icon package doesn't seem to provide an easy way to get at
+# them, so we do a little reimplementing here).
+_icon_handles = {}
+
+def _get_icon_handle(file_path):
+ global _icon_handles
+
+ if file_path not in _icon_handles:
+ with open(file_path, 'r') as f:
+ data = f.read()
+ _icon_handles[file_path] = rsvg.Handle(data=data)
+
+ return _icon_handles[file_path]
diff --git a/icons/help-icon.svg b/icons/help-icon.svg
new file mode 100644
index 0000000..f6c92bf
--- /dev/null
+++ b/icons/help-icon.svg
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="55px" height="55px">
+ <path
+ style="fill:none;stroke:#ffffff;stroke-width:3;stroke-linejoin:round"
+ d="M 48,28 A 20,20 0 1 1 8,28 A 20,20 0 1 1 48,28 z"/>
+ <path
+ style="fill:none;stroke:#ffffff;stroke-width:6;stroke-linecap:round;stroke-linejoin:round"
+ d="M 22,20 C 22,20 25,17 29,17 C 33,17 36,19 36,23 C 36,27 31,29 28,29 L 28,32" />
+ <path
+ style="fill:#ffffff"
+ d="M 25,40
+ a 3,3 0 1 1 6,0
+ a 3,3 0 1 1 -6,0 z" />
+</svg>
diff --git a/implodeactivity.py b/implodeactivity.py
index 41e7aeb..5a19867 100644
--- a/implodeactivity.py
+++ b/implodeactivity.py
@@ -21,13 +21,17 @@ _logger = logging.getLogger('implode-activity')
from gettext import gettext as _
-from sugar.activity.activity import Activity, ActivityToolbox
-from sugar.graphics.toolbutton import ToolButton
+from sugar.activity.activity import Activity, ActivityToolbox, get_bundle_path
+from sugar.graphics import style
+from sugar.graphics.icon import Icon
from sugar.graphics.radiotoolbutton import RadioToolButton
+from sugar.graphics.toolbutton import ToolButton
-import implodegame
+from implodegame import ImplodeGame
+from helpwidget import HelpWidget
import os
+
try:
import json
json.dumps
@@ -50,7 +54,7 @@ class ImplodeActivity(Activity):
_logger.debug('Starting implode activity...')
- self._game = implodegame.ImplodeGame()
+ self._game = ImplodeGame()
toolbox = _Toolbox(self)
self.set_toolbox(toolbox)
@@ -71,6 +75,8 @@ class ImplodeActivity(Activity):
self._game.set_level(level)
toolbox.connect(signal, callback)
+ toolbox.connect('help-clicked', self._help_clicked_cb)
+
self.set_canvas(self._game)
self.show_all()
self._game.grab_focus()
@@ -109,6 +115,13 @@ class ImplodeActivity(Activity):
f.write(content)
f.close()
+ def _help_clicked_cb(self, source):
+ help_window = _HelpWindow()
+ help_window.set_transient_for(self.get_toplevel())
+ help_window.show_all()
+ self.present()
+
+
class _Toolbox(ActivityToolbox):
__gsignals__ = {
'new-game-clicked' : (gobject.SIGNAL_RUN_LAST, None, ()),
@@ -118,6 +131,7 @@ class _Toolbox(ActivityToolbox):
'easy-clicked' : (gobject.SIGNAL_RUN_LAST, None, ()),
'medium-clicked' : (gobject.SIGNAL_RUN_LAST, None, ()),
'hard-clicked' : (gobject.SIGNAL_RUN_LAST, None, ()),
+ 'help-clicked' : (gobject.SIGNAL_RUN_LAST, None, ()),
}
def __init__(self, activity):
@@ -162,5 +176,156 @@ class _Toolbox(ActivityToolbox):
add_level_button('medium-level', _("Medium"), 'medium-clicked')
add_level_button('hard-level' , _("Hard") , 'hard-clicked')
+ separator = gtk.SeparatorToolItem()
+ separator.set_expand(True)
+ separator.set_draw(False)
+ toolbar.add(separator)
+
+ # NOTE: Naming the icon "help" instead of "help-icon" seems to use a
+ # GTK stock icon instead of our custom help; the stock icon may be more
+ # desireable in the future. It doesn't seem to be themed for Sugar
+ # right now, however.
+ add_button('help-icon', _("Help"), 'help-clicked')
+
self.add_toolbar(_("Game"), toolbar)
self.set_current_toolbar(1)
+
+
+class _HelpWindow(gtk.Window):
+ def __init__(self):
+ super(_HelpWindow, self).__init__()
+
+ self.set_border_width(style.LINE_WIDTH)
+ offset = style.GRID_CELL_SIZE
+ width = gtk.gdk.screen_width() - offset * 2
+ height = gtk.gdk.screen_height() - offset * 2
+ self.set_size_request(width, height)
+ self.set_position(gtk.WIN_POS_CENTER_ALWAYS)
+ self.set_decorated(False)
+ self.set_resizable(False)
+ self.set_modal(True)
+
+ vbox = gtk.VBox()
+ self.add(vbox)
+
+ toolbar = _HelpToolbar()
+ toolbar.connect('stop-clicked', self._stop_clicked_cb)
+
+ vbox.pack_start(toolbar, False)
+
+ self._help_widget = HelpWidget(self._icon_file)
+ vbox.pack_start(self._help_widget)
+
+ self._help_nav_bar = _HelpNavBar()
+ vbox.pack_end(self._help_nav_bar,
+ expand=False,
+ padding=style.DEFAULT_SPACING)
+
+ for (signal_name, callback) in [
+ ('forward-clicked', self._forward_clicked_cb),
+ ('reload-clicked', self._reload_clicked_cb),
+ ('back-clicked', self._back_clicked_cb)]:
+ self._help_nav_bar.connect(signal_name, callback)
+
+ self._update_prev_next()
+
+ def _stop_clicked_cb(self, source):
+ self.destroy()
+
+ def _forward_clicked_cb(self, source):
+ self._help_widget.next_stage()
+ self._update_prev_next()
+
+ def _back_clicked_cb(self, source):
+ self._help_widget.prev_stage()
+ self._update_prev_next()
+
+ def _reload_clicked_cb(self, source):
+ self._help_widget.replay_stage()
+
+ def _icon_file(self, icon_name):
+ activity_path = get_bundle_path()
+ file_path = os.path.join(activity_path, 'icons', icon_name + '.svg')
+ return file_path
+
+ def _update_prev_next(self):
+ hw = self._help_widget
+ self._help_nav_bar.set_can_prev_stage(hw.can_prev_stage())
+ self._help_nav_bar.set_can_next_stage(hw.can_next_stage())
+
+
+class _HelpToolbar(gtk.Toolbar):
+ __gsignals__ = {
+ 'stop-clicked' : (gobject.SIGNAL_RUN_LAST, None, ()),
+ }
+ def __init__(self):
+ super(_HelpToolbar, self).__init__()
+
+ icon = Icon()
+ icon.set_from_icon_name('help-icon', gtk.ICON_SIZE_LARGE_TOOLBAR)
+ self._add_widget(icon)
+
+ self._add_separator()
+
+ label = gtk.Label(_("Help"))
+ self._add_widget(label)
+
+ self._add_separator(expand=True)
+
+ stop = ToolButton(icon_name='dialog-cancel')
+ stop.set_tooltip(_('Done'))
+ stop.connect('clicked', self._stop_clicked_cb)
+ self.add(stop)
+
+ def _add_separator(self, expand=False):
+ separator = gtk.SeparatorToolItem()
+ separator.set_expand(expand)
+ separator.set_draw(False)
+ self.add(separator)
+
+ def _add_widget(self, widget):
+ tool_item = gtk.ToolItem()
+ tool_item.add(widget)
+ self.add(tool_item)
+
+ def _stop_clicked_cb(self, button):
+ self.emit('stop-clicked')
+
+
+class _HelpNavBar(gtk.HButtonBox):
+ __gsignals__ = {
+ 'forward-clicked' : (gobject.SIGNAL_RUN_LAST, None, ()),
+ 'back-clicked' : (gobject.SIGNAL_RUN_LAST, None, ()),
+ 'reload-clicked' : (gobject.SIGNAL_RUN_LAST, None, ()),
+ }
+
+ def __init__(self):
+ super(_HelpNavBar, self).__init__()
+
+ self.set_layout(gtk.BUTTONBOX_SPREAD)
+
+ def add_button(icon_name, tooltip, signal_name):
+ icon = Icon()
+ icon.set_from_icon_name(icon_name, gtk.ICON_SIZE_LARGE_TOOLBAR)
+ button = gtk.Button()
+ button.set_image(icon)
+ button.set_tooltip_text(tooltip)
+ self.add(button)
+
+ def callback(source):
+ self.emit(signal_name)
+ button.connect('clicked', callback)
+
+ return button
+
+ self._back_button = add_button('back', _("Previous"), 'back-clicked')
+ add_button('reload', _("Again"), 'reload-clicked')
+ self._forward_button = add_button('forward', _("Next"), 'forward-clicked')
+
+ def set_can_prev_stage(self, can_prev_stage):
+ self._back_button.set_sensitive(can_prev_stage)
+
+ def set_can_next_stage(self, can_next_stage):
+ self._forward_button.set_sensitive(can_next_stage)
+
+
diff --git a/sugarless.py b/sugarless.py
index 118f828..3a5832e 100644
--- a/sugarless.py
+++ b/sugarless.py
@@ -22,8 +22,12 @@
import pygtk
pygtk.require('2.0')
import gtk
+import gobject
+
+import os
import implodegame
+from helpwidget import HelpWidget
class ImplodeWindow(gtk.Window):
def __init__(self):
@@ -73,6 +77,13 @@ class ImplodeWindow(gtk.Window):
for button in radio_buttons[1:]:
button.set_group(radio_buttons[0])
+ separator = gtk.SeparatorToolItem()
+ separator.set_expand(True)
+ separator.set_draw(False)
+ toolbar.add(separator)
+
+ add_button(gtk.STOCK_HELP, self._help_clicked)
+
main_box = gtk.VBox(False, 0)
main_box.pack_start(toolbar, False)
main_box.pack_start(self.game, True, True, 0)
@@ -86,13 +97,101 @@ class ImplodeWindow(gtk.Window):
return False
def _easy_clicked(self):
- print "Easy"
+ self.game.set_level(0)
def _medium_clicked(self):
- print "Medium"
+ self.game.set_level(1)
def _hard_clicked(self):
- print "Hard"
+ self.game.set_level(2)
+
+ def _help_clicked(self):
+ help_window = _HelpWindow()
+ help_window.set_transient_for(self.get_toplevel())
+ help_window.show_all()
+
+
+class _HelpWindow(gtk.Window):
+ def __init__(self):
+ super(_HelpWindow, self).__init__()
+
+ self.set_size_request(640, 480)
+ self.set_position(gtk.WIN_POS_CENTER_ON_PARENT)
+ self.set_modal(True)
+
+ vbox = gtk.VBox()
+ self.add(vbox)
+
+ self._help_widget = HelpWidget(self._icon_file)
+ vbox.pack_start(self._help_widget)
+
+ self._help_nav_bar = _HelpNavBar()
+ vbox.pack_end(self._help_nav_bar,
+ expand=False)
+
+ for (signal_name, callback) in [
+ ('forward-clicked', self._forward_clicked_cb),
+ ('reload-clicked', self._reload_clicked_cb),
+ ('back-clicked', self._back_clicked_cb)]:
+ self._help_nav_bar.connect(signal_name, callback)
+
+ self._update_prev_next()
+
+ def _stop_clicked_cb(self, source):
+ self.destroy()
+
+ def _forward_clicked_cb(self, source):
+ self._help_widget.next_stage()
+ self._update_prev_next()
+
+ def _back_clicked_cb(self, source):
+ self._help_widget.prev_stage()
+ self._update_prev_next()
+
+ def _reload_clicked_cb(self, source):
+ self._help_widget.replay_stage()
+
+ def _icon_file(self, icon_name):
+ return os.path.join('icons', icon_name + '.svg')
+
+ def _update_prev_next(self):
+ hw = self._help_widget
+ self._help_nav_bar.set_can_prev_stage(hw.can_prev_stage())
+ self._help_nav_bar.set_can_next_stage(hw.can_next_stage())
+
+
+class _HelpNavBar(gtk.HButtonBox):
+ __gsignals__ = {
+ 'forward-clicked' : (gobject.SIGNAL_RUN_LAST, None, ()),
+ 'back-clicked' : (gobject.SIGNAL_RUN_LAST, None, ()),
+ 'reload-clicked' : (gobject.SIGNAL_RUN_LAST, None, ()),
+ }
+
+ def __init__(self):
+ super(_HelpNavBar, self).__init__()
+
+ self.set_layout(gtk.BUTTONBOX_SPREAD)
+
+ def add_button(id, signal_name):
+ button = gtk.Button(stock=id)
+ self.add(button)
+
+ def callback(source):
+ self.emit(signal_name)
+ button.connect('clicked', callback)
+
+ return button
+
+ self._back_button = add_button(gtk.STOCK_GO_BACK, 'back-clicked')
+ add_button(gtk.STOCK_MEDIA_PLAY, 'reload-clicked')
+ self._forward_button = add_button(gtk.STOCK_GO_FORWARD, 'forward-clicked')
+
+ def set_can_prev_stage(self, can_prev_stage):
+ self._back_button.set_sensitive(can_prev_stage)
+
+ def set_can_next_stage(self, can_next_stage):
+ self._forward_button.set_sensitive(can_next_stage)
+
def main():