From d3ece019e1623ef36b1b4737a6c63343367f3366 Mon Sep 17 00:00:00 2001
From: Paolo Capriotti
Date: Sun, 21 Jun 2009 22:10:50 +0200
Subject: [PATCH] Implemented drag and drop.
---
lib/animations.rb | 12 +++--
lib/animator_helper.rb | 3 +-
lib/board/board.rb | 37 ++++++++++++++--
lib/board/pool.rb | 17 ++++++-
lib/board/scene.rb | 97 ++++++++++++++++++++++++++++++++++++++--
lib/controller.rb | 106 +++++++++++++++++++++++++++++++++++++++++---
lib/games/chess/animator.rb | 32 +++++++++----
lib/games/chess/policy.rb | 8 +++-
lib/games/shogi/policy.rb | 6 +--
lib/games/shogi/state.rb | 2 +-
lib/item.rb | 11 +++++
lib/mainwindow.rb | 2 +-
test/test_animations.rb | 6 +++
13 files changed, 307 insertions(+), 32 deletions(-)
diff --git a/lib/animations.rb b/lib/animations.rb
index 6787ac6..21bbbc3 100644
--- a/lib/animations.rb
+++ b/lib/animations.rb
@@ -29,13 +29,19 @@ module Animations
def movement(item, src, dst, path_factory)
if item
- src = board.to_real(src)
+ src = if src
+ board.to_real(src)
+ else
+ item.pos
+ end
+
dst = board.to_real(dst)
path = path_factory.new(src, dst)
- SimpleAnimation.new "move to #{dst}", LENGTH, nil,
+ SimpleAnimation.new "move to #{dst}", LENGTH,
+ lambda { board.raise(item) },
lambda {|i| item.pos = src + path[i] },
- lambda { item.pos = dst }
+ lambda { item.pos = dst; board.lower(item) }
end
end
diff --git a/lib/animator_helper.rb b/lib/animator_helper.rb
index 40ea8fd..1f16f2a 100644
--- a/lib/animator_helper.rb
+++ b/lib/animator_helper.rb
@@ -3,8 +3,9 @@ require 'animations'
module AnimatorHelper
include Animations
- def move!(src, dst, path)
+ def move!(src, dst, path, opts = {})
piece = board.move_item(src, dst)
+ src = nil if opts[:adjust]
movement(piece, src, dst, path)
end
diff --git a/lib/board/board.rb b/lib/board/board.rb
index f7a342d..f9cecab 100644
--- a/lib/board/board.rb
+++ b/lib/board/board.rb
@@ -5,8 +5,6 @@ require 'item'
require 'board/item_bag'
class Board < Qt::GraphicsItemGroup
- BACKGROUND_ZVALUE = -10
-
include TaggableSquares
include Observable
include PointConverter
@@ -112,9 +110,40 @@ class Board < Qt::GraphicsItemGroup
add_item p, @theme.pieces.pixmap(piece, @unit), opts
end
- def on_click(pos)
+ def create_piece(piece, opts = {})
+ opts = opts.merge :name => piece
+ create_item p, @theme.pieces.pixmap(piece, @unit), opts
+ end
+
+ def on_click(pos, press_pos)
+ p = to_logical(pos)
+ p2 = to_logical(press_pos)
+
+ if p == p2
+ fire :click => p
+ end
+ end
+
+ def on_drag(pos)
p = to_logical(pos)
- fire :click => p
+ item = items[p]
+ if item
+ fire :drag => { :src => p,
+ :item => item,
+ :size => @unit }
+ end
+ end
+
+ def on_drop(old_pos, pos, data)
+ if data[:item]
+ src = if old_pos
+ to_logical(old_pos)
+ end
+ dst = if pos
+ to_logical(pos)
+ end
+ fire :drop => data.merge(:src => src, :dst => dst)
+ end
end
def highlight(move)
diff --git a/lib/board/pool.rb b/lib/board/pool.rb
index 713d62b..dc6c284 100644
--- a/lib/board/pool.rb
+++ b/lib/board/pool.rb
@@ -68,8 +68,23 @@ class Pool < Qt::GraphicsItemGroup
item
end
- def on_click(pos)
+ def on_click(pos, press_pos)
+
+ end
+
+ def on_drag(pos)
index = to_logical(pos)
+ item = items[index]
+ if item
+ fire :drag => { :index => index,
+ :item => item }
+ end
+ end
+
+ def on_drop(old_pos, pos, data)
+ if data[:item]
+ fire :drop => data
+ end
end
def to_logical(p)
diff --git a/lib/board/scene.rb b/lib/board/scene.rb
index 29db2c2..b98e178 100644
--- a/lib/board/scene.rb
+++ b/lib/board/scene.rb
@@ -1,4 +1,9 @@
+require 'observer_utils'
+
class Scene < Qt::GraphicsScene
+ MINIMAL_DRAG_DISTANCE = 3
+ include Observer
+
def initialize
super
@@ -12,11 +17,97 @@ class Scene < Qt::GraphicsScene
def mousePressEvent(e)
if e.button == Qt::LeftButton
pos = e.scene_pos.to_i
- @elements.each do |element|
- if element.rect.contains(pos)
- element.on_click(pos - element.rect.top_left)
+ if find_element(pos)
+ @drag_data = { :pos => pos }
+ end
+ end
+ end
+
+ def mouseReleaseEvent(e)
+ if e.button == Qt::LeftButton
+ if @drag_data
+ old_pos = @drag_data[:pos]
+ item = @drag_data[:item]
+ data = @drag_data
+ @drag_data = nil
+
+ pos = e.scene_pos.to_i
+ element_src = find_element(old_pos)
+ element_dst = find_element(pos)
+
+ if data[:dragging]
+ # normal drag and drop
+
+ if element_dst.nil?
+ # if the drop is in a blank area,
+ # notify the source of the drop
+ notify(element_src, :drop, [old_pos, nil], data)
+ else
+ src = if element_src == element_dst
+ old_pos
+ end
+ notify(element_dst, :drop, [src, pos], data)
+ end
+ elsif element_src == element_dst
+ # close drag and drop == click
+ # the element will decide how to handle it based on the distance
+ # between the coordinates
+ notify(element_dst, :click, [old_pos, pos])
+ else
+ # a rapid drag and drop between different elements
+ # is never considered a click
+ notify(element_src, :drag, [old_pos])
+ notify(element_src, :drop, [nil, pos], data)
+ end
+ end
+ end
+ end
+
+ def mouseMoveEvent(e)
+ if @drag_data
+ pos = e.scene_pos.to_i
+ if !@drag_data[:dragging]
+ dx = (@drag_data[:pos].x - pos.x).abs
+ dy = (@drag_data[:pos].y - pos.y).abs
+ if dx >= MINIMAL_DRAG_DISTANCE ||
+ dy >= MINIMAL_DRAG_DISTANCE
+ @drag_data[:dragging] = true
+ notify(find_element(pos), :drag, [@drag_data[:pos]])
+ else
+ return
end
end
+
+ if @drag_data[:item]
+ @drag_data[:item].pos = (pos - @drag_data[:size] / 2).to_f
+ end
+ end
+ end
+
+ def find_element(pos)
+ @elements.detect do |element|
+ element.rect.contains(pos)
+ end
+ end
+
+ def notify(element, event, pos, *args)
+ if element
+ relpos = pos.map{|p| rel(element, p) }
+ element.send("on_#{event}", *(relpos + args))
+ end
+ end
+
+ def rel(element, pos)
+ if pos
+ pos - element.rect.top_left
+ end
+ end
+
+ # invoked by the controller when one of the elements
+ # accepts a drag
+ def on_drag(data)
+ if @drag_data
+ @drag_data = @drag_data.merge(data)
end
end
end
diff --git a/lib/controller.rb b/lib/controller.rb
index 2b126fc..7428455 100644
--- a/lib/controller.rb
+++ b/lib/controller.rb
@@ -7,7 +7,8 @@ class Controller
attr_reader :history
- def initialize(elements, game, history)
+ def initialize(scene, elements, game, history)
+ @scene = scene
@board = elements[:board]
@pools = elements[:pools]
@@ -19,6 +20,12 @@ class Controller
c = self
@board.observe(:click) {|p| c.on_board_click(p) }
+ @board.observe(:drag) {|data| c.on_board_drag(data) }
+ @board.observe(:drop) {|data| c.on_board_drop(data) }
+ @pools.each do |color, pool|
+ pool.observe(:drag) {|data| c.on_pool_drag(color, data) }
+ pool.observe(:drop) {|data| c.on_pool_drop(color, data) }
+ end
end
def on_board_click(p)
@@ -36,11 +43,11 @@ class Controller
end
end
- def perform!(move)
+ def perform!(move, opts = {})
state = @history.state.dup
state.perform! move
@history.add_move(state, move)
- animate(:forward, state, move)
+ animate(:forward, state, move, opts)
@board.highlight(move)
end
@@ -60,8 +67,8 @@ class Controller
puts "error: last move"
end
- def animate(direction, state, move)
- anim = @animator.send(direction, state, move)
+ def animate(direction, state, move, opts = {})
+ anim = @animator.send(direction, state, move, opts)
@field.run anim
update_pools
@@ -74,7 +81,96 @@ class Controller
end
end
+ def on_board_drop(data)
+ if data[:src]
+ move = nil
+
+ if data[:src] == data[:dst]
+ @board.selection = data[:src]
+ elsif data[:dst]
+ # normal move
+ move = @game.policy.new_move(@history.state, data[:src], data[:dst])
+ validate = @game.validator.new(@history.state)
+ validate[move]
+ end
+
+ if move and move.valid?
+ @board.add_to_group data[:item]
+ @board.lower data[:item]
+ perform! move, :adjust => true
+ else
+ cancel_drop(data)
+ end
+ elsif data[:index] and data[:dst]
+ # actual drop
+ move = @game.policy.new_move(@history.state, nil,
+ data[:dst], :dropped => data[:item].name)
+ validate = @game.validator.new(@history.state)
+ if validate[move]
+ @board.add_to_group data[:item]
+ @board.lower data[:item]
+ perform! move, :dropped => data[:item]
+ else
+ cancel_drop(data)
+ end
+ end
+ end
+
+ def on_board_drag(data)
+ if @game.policy.movable?(@history.state, data[:src]) and
+ movable?(data[:src])
+ @board.raise data[:item]
+ @board.remove_from_group data[:item]
+ @board.selection = nil
+ @scene.on_drag(data)
+ end
+ end
+
+ def on_pool_drag(color, data)
+ if @game.policy.droppable?(@history.state, color, data[:index]) and
+ droppable?(color, data[:index])
+
+ # replace item with a correctly sized one
+ item = @board.create_piece(data[:item].name)
+ @board.raise item
+ @board.remove_from_group item
+ anim = @pools[color].animator.remove_piece(data[:index])
+ data[:item] = item
+ data[:size] = @board.unit
+ data[:pool_color] = color
+
+ @scene.on_drag(data)
+
+ @field.run anim
+ end
+ end
+
+ def on_pool_drop(color, data)
+ cancel_drop(data)
+ end
+
+ def cancel_drop(data)
+ anim = if data[:index]
+ # remove dragged item
+ data[:item].remove
+ # make original item reappear in its place
+ @pools[data[:pool_color]].animator.insert_piece(
+ data[:index],
+ data[:item].name)
+ elsif data[:src]
+ @board.add_to_group data[:item]
+ @board.lower data[:item]
+ @animator.movement(data[:item], nil, data[:src], Path::Linear)
+ end
+
+ @field.run(anim) if anim
+ end
+
def movable?(p)
true
end
+
+ def droppable?(color, index)
+ true
+ end
end
diff --git a/lib/games/chess/animator.rb b/lib/games/chess/animator.rb
index 3e83210..c5eac8e 100644
--- a/lib/games/chess/animator.rb
+++ b/lib/games/chess/animator.rb
@@ -9,13 +9,13 @@ module Chess
@board = board
end
- def specific_move!(piece, src, dst)
- path = if piece and piece.type == :knight
+ def specific_move!(piece, src, dst, opts = {})
+ path = if piece and piece.type == :knight and (not opts[:adjust])
Path::LShape
else
Path::Linear
end
- move!(src, dst, path)
+ move!(src, dst, path, opts)
end
def warp(state, opts = { :instant => true })
@@ -39,10 +39,21 @@ module Chess
group(*res)
end
- def forward(state, move)
+ def forward(state, move, opts = {})
piece = state.board[move.dst]
capture = disappear_on! move.dst
- actual_move = specific_move! piece, move.src, move.dst
+
+ actual_move = if move.src.nil?
+ if opts[:dropped]
+ @board.items[move.dst] = opts[:dropped]
+ movement opts[:dropped], nil, move.dst, Path::Linear
+ elsif move.respond_to?(:dropped)
+ appear_on! move.dst, move.dropped
+ end
+ else
+ specific_move! piece, move.src, move.dst, opts
+ end
+
extra = if move.type == :king_side_castling
specific_move! piece, move.dst + Point.new(1, 0), move.dst - Point.new(1, 0)
elsif move.type == :queen_side_castling
@@ -55,9 +66,14 @@ module Chess
sequence(main, rest)
end
- def back(state, move)
- piece = state.board[move.src]
- actual_move = specific_move! piece, move.dst, move.src
+ def back(state, move, opts = {})
+ actual_move = if move.src.nil?
+ disappear_on! move.dst
+ else
+ piece = state.board[move.src]
+ specific_move! piece, move.dst, move.src
+ end
+
extra = if move.type == :king_side_castling
specific_move! piece, move.dst - Point.new(1, 0), move.dst + Point.new(1, 0)
elsif move.type == :queen_side_castling
diff --git a/lib/games/chess/policy.rb b/lib/games/chess/policy.rb
index bcfe657..8ddc51d 100644
--- a/lib/games/chess/policy.rb
+++ b/lib/games/chess/policy.rb
@@ -9,8 +9,12 @@ module Chess
piece && piece.color == state.turn
end
- def new_move(state, src, dst)
- @move_factory.new(src, dst, :promotion => :queen)
+ def droppable?(state, color, index)
+ color == state.turn
+ end
+
+ def new_move(state, src, dst, opts = {})
+ @move_factory.new(src, dst, opts.merge(:promotion => :queen))
end
end
end
diff --git a/lib/games/shogi/policy.rb b/lib/games/shogi/policy.rb
index 9afc381..efd66d9 100644
--- a/lib/games/shogi/policy.rb
+++ b/lib/games/shogi/policy.rb
@@ -8,10 +8,10 @@ class Policy < Chess::Policy
@validator_factory = validator_factory
end
- def new_move(state, src, dst)
- move = @move_factory.new(src, dst, :promote => true)
+ def new_move(state, src, dst, opts = {})
+ move = @move_factory.new(src, dst, opts.merge(:promote => true))
valid = @validator_factory.new(state)
- move = @move_factory.new(src, dst, :promote => false) unless valid[move]
+ move = @move_factory.new(src, dst, opts.merge(:promote => false)) unless valid[move]
move
end
end
diff --git a/lib/games/shogi/state.rb b/lib/games/shogi/state.rb
index 03dee3a..0bbcd96 100644
--- a/lib/games/shogi/state.rb
+++ b/lib/games/shogi/state.rb
@@ -80,8 +80,8 @@ module Shogi
piece = piece_factory.new(turn, captured.type)
pool(turn).add(demoted(piece))
end
- switch_turn!
end
+ switch_turn!
end
def switch_turn!
diff --git a/lib/item.rb b/lib/item.rb
index 6ded8e0..bca8f81 100644
--- a/lib/item.rb
+++ b/lib/item.rb
@@ -31,6 +31,9 @@ class Item < Qt::GraphicsPixmapItem
end
module ItemUtils
+ BACKGROUND_ZVALUE = -10
+ TEMP_ZVALUE = 10
+
def create_item(key, pix, opts = {})
name = opts[:name] || key.to_s
item = Item.new(name, pix, self, scene)
@@ -43,4 +46,12 @@ module ItemUtils
def destroy_item(item)
scene.remove_item item
end
+
+ def raise(item)
+ item.z_value = TEMP_ZVALUE
+ end
+
+ def lower(item)
+ item.z_value = 0
+ end
end
diff --git a/lib/mainwindow.rb b/lib/mainwindow.rb
index caff6da..1162677 100644
--- a/lib/mainwindow.rb
+++ b/lib/mainwindow.rb
@@ -71,7 +71,7 @@ private
table = Table.new scene, theme, self, elements
history = History.new(state)
- @controller = Controller.new(elements, game, history)
+ @controller = Controller.new(scene, elements, game, history)
self.central_widget = table
end
diff --git a/test/test_animations.rb b/test/test_animations.rb
index 4f166e9..66a33e1 100644
--- a/test/test_animations.rb
+++ b/test/test_animations.rb
@@ -17,6 +17,12 @@ class FakeAnimator
def flipped?
false
end
+
+ def raise(item)
+ end
+
+ def lower(item)
+ end
end
attr_reader :board
--
2.11.4.GIT