diff options
Diffstat (limited to 'helpwidget.py')
-rw-r--r-- | helpwidget.py | 891 |
1 files changed, 891 insertions, 0 deletions
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] |