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