bugfix: safety against breaking the AI if you press undo while it's thinking
[kaya.git] / lib / mainwindow.rb
blobee72f3587c6909efcb79338d619b576a8e3173bd
1 # Copyright (c) 2009 Paolo Capriotti <p.capriotti@gmail.com>
2
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
8 require 'toolkit'
9 require 'board/board'
10 require 'board/pool'
11 require 'board/table'
12 require 'board/scene'
13 require 'interaction/history'
14 require 'controller'
15 require 'dummy_player'
17 require 'interaction/match'
19 require 'console'
21 require 'filewriter'
22 require 'newgame'
23 require 'engine_prefs'
24 require 'theme_prefs'
25 require 'view'
26 require 'multiview'
27 require 'status_bar'
29 require 'kaya_ui'
31 class MainWindow < KDE::XmlGuiWindow
32   include ActionHandler
33   include FileWriter
34   
35   attr_reader :console
36   attr_reader :view
38   def initialize(loader, game)
39     super nil
40     
41     @loader = loader
42     @theme_loader = @loader.get_matching(:theme_loader).new
43     
44     @factories = Hash.new do |h, interface|
45       @loader.get_matching(interface)
46     end
47     @factories[:table] = Factory.new do |parent|
48         Table.new(Scene.new, @loader, @theme_loader, parent)
49       end
50     @factories[:controller] = Factory.new do |parent|
51       Controller.new(parent, @field).tap do |c|
52         c.on(:changed_active_actions) do
53           update_active_actions(c)
54         end
55         c.on(:reset) do
56           update_game_actions(c)
57         end
58       end
59     end
61     startup(game)
62     setup_actions
63     load_action_providers
64     setGUI(Kaya::GUI)
65     new_game(Match.new(game), :new_tab => false)
66   end
67   
68   def closeEvent(event)
69     saveGUI
70     if controller.match
71       controller.match.close
72     end
73     event.accept
74   end
76   def controller
77     @view.current.controller
78   end
80 private
82   def setup_actions
83     @actions = { }
84     regular_action(:open_new,
85                    :icon => 'document-new',
86                    :text => KDE::i18n("&New..."),
87                    :shortcut => KDE::std_shortcut(:new),
88                    :tooltip => KDE::i18n("Start a new game...")) do
89       create_game
90     end
91     
92     regular_action(:open,
93                    :icon => 'document-open',
94                    :text => KDE::i18n("&Load..."),
95                    :shortcut => KDE::std_shortcut(:open),
96                    :tooltip => KDE::i18n("Open a saved game...")) do
97       load_game
98     end
99     
100     regular_action(:quit,
101                    :icon => 'application-exit',
102                    :text => KDE::i18n("&Quit"),
103                    :shortcut => KDE::std_shortcut(:quit),
104                    :tooltip => KDE::i18n("Quit the program")) do
105       close
106     end
107     
108     regular_action(:save,
109                    :icon => 'document-save',
110                    :text => KDE::i18n("&Save"),
111                    :shortcut => KDE::std_shortcut(:save),
112                    :tooltip => KDE::i18n("Save the current game")) do
113       save_game
114     end
115     
116     regular_action(:save_as,
117                    :icon => 'document-save-as',
118                    :text => KDE::i18n("Save &As..."),
119                    :tooltip => KDE::i18n("Save the current game to another file")) do
120       save_game_as
121     end
122     
123     @actions[:back] = regular_action :back, :icon => 'go-previous', 
124                           :text => KDE.i18n("B&ack") do
125       controller.back
126     end
127     @actions[:forward] = regular_action :forward, :icon => 'go-next', 
128                              :text => KDE.i18n("&Forward") do
129       controller.forward
130     end
131     
132     regular_action :flip, :icon => 'object-rotate-left',
133                           :text => KDE.i18n("F&lip") do
134       table = @view.current.table
135       table.flip(!table.flipped?)
136     end
137     
138     regular_action :configure_engines,
139                    :icon => 'help-hint',
140                    :text => KDE.i18n("Configure &Engines...") do
141       dialog = EnginePrefs.new(@engine_loader, self)
142       dialog.show
143     end
144     
145     regular_action :configure_themes,
146                    :icon => 'games-config-theme',
147                    :text => KDE.i18n("Configure &Themes...") do
148       dialog = ThemePrefs.new(@loader, @theme_loader, self)
149       dialog.on(:ok) { controller.reset }
150       dialog.show
151     end
152     
153     @actions[:undo] = regular_action(:undo,
154             :icon => 'edit-undo',
155             :text => KDE::i18n("Und&o"),
156             :shortcut => KDE::std_shortcut(:undo),
157             :tooltip => KDE::i18n("Undo the last history operation")) do
158       controller.undo!
159     end
161     @actions[:redo] = regular_action(:redo,
162             :icon => 'edit-redo',
163             :text => KDE::i18n("Re&do"),
164             :shortcut => KDE::std_shortcut(:redo),
165             :tooltip => KDE::i18n("Redo the last history operation")) do
166       controller.redo!
167     end
168   end
169   
170   def load_action_providers
171     @loader.get_all_matching(:action_provider).each do |provider_klass|
172       provider = provider_klass.new
173       ActionProviderClient.new(self, provider)
174     end
175   end
176   
177   def startup(game)
178     @field = AnimationField.new(20)
180     movelist_stack = Qt::StackedWidget.new(self)
181     movelist_dock = Qt::DockWidget.new(self)
182     movelist_dock.widget = movelist_stack
183     movelist_dock.window_title = KDE.i18n("History")
184     movelist_dock.object_name = "movelist"
185     add_dock_widget(Qt::LeftDockWidgetArea, movelist_dock, Qt::Vertical)
186     movelist_dock.show
187     action_collection[:toggle_history] = movelist_dock.toggle_view_action
189     @view = MultiView.new(self, movelist_stack, @factories)
190     @view.create(:name => game.class.plugin_name)
191     @view.on(:changed) do
192       update_active_actions(controller)
193       update_title
194       update_game_actions(controller)
195     end
196     @view.clean = true
198     update_title
199     
200     @engine_loader = @loader.get_matching(:engine_loader).new
201     @engine_loader.reload
202     
203     @console = Console.new(nil)
204     console_dock = Qt::DockWidget.new(self)                                                      
205     console_dock.widget = @console                                                             
206     console_dock.focus_proxy = @console                                                        
207     console_dock.window_title = KDE.i18n("Console")                                              
208     console_dock.object_name = "console"                                                         
209     add_dock_widget(Qt::BottomDockWidgetArea, console_dock, Qt::Horizontal)                      
210     console_dock.window_flags = console_dock.window_flags & ~Qt::WindowStaysOnTopHint            
211     console_dock.show
212     action_collection[:toggle_console] = console_dock.toggle_view_action
214     self.status_bar = StatusBar.new(self)
216     self.central_widget = @view
217   end
218   
219   def new_game(match, opts = { })
220     setup_single_player(match)
221     controller.reset(match)
222   end
223   
224   def setup_single_player(match)
225     controller.color = match.game.players.first
226     controller.premove = false
227     opponents = match.game.players[1..-1].map do |color|
228       DummyPlayer.new(color)
229     end
230     opponents.each do |p| 
231       controller.add_controlled_player(p)
232     end
234     controller.controlled.values.each do |p|
235       match.register(p)
236     end
237     controller.controlled.values.each do |p|
238       match.start(p)
239     end
240   end
242   def create_game(opts = { })
243     current_game = if controller.match 
244       controller.match.game
245     end
246     diag = NewGame.new(self, @engine_loader, current_game)
247     diag.on(:ok) do |data|
248       game = data[:game]
249       match = Match.new(game, :editable => data[:engines].empty?)
250       if data[:new_tab]
251         @view.create(:activate => true,
252                      :force => true,
253                      :name => game.class.plugin_name)
254       else
255         @view.set_tab_text(@view.index, game.class.plugin_name)
256         update_title
257       end
258       contr = controller
259       
260       
261       match.on(:started) do
262         contr.reset(match)
263       end
264       
265       # set up engine players
266       players = game.players
267       data[:engines].each do |player, engine|
268         e = engine.new(player, match)
269         e.start
270       end
271       
272       # set up human players
273       if data[:humans].empty?
274         contr.color = nil
275       else
276         contr.color = data[:humans].first
277         contr.premove = data[:humans].size == 1
278         match.register(contr)
279         
280         data[:humans][1..-1].each do |player|
281           p = DummyPlayer.new(player)
282           contr.add_controlled_player(p)
283           match.register(p)
284         end
285       end
286       contr.controlled.values.each {|p| match.start(p) }
287     end
288     diag.show
289   end
291   def load_game
292     url = KDE::FileDialog.get_open_url(KDE::Url.new, '*.*', self,
293       KDE.i18n("Open game"))
294     
295     return if url.is_empty or (not url.path)
296     
297     puts "url = #{url.inspect}"
298     # find readers
299     ext = File.extname(url.path)[1..-1]
300     return unless ext
301     readers = Game.to_enum.find_all do |_, game|
302       game.respond_to?(:game_extensions) and
303       game.game_extensions.include?(ext)
304     end.map do |_, game|
305       [game, game.game_reader]
306     end
307     
308     if readers.empty?
309       warn "Unknown file extension #{ext}"
310       return
311     end
312     
313     tmp_file = KDE::download_tempfile(url, self)
314     return unless tmp_file
316     history = nil
317     game = nil
318     info = nil
319     
320     readers.each do |g, reader|
321       begin
322         data = File.open(tmp_file) do |f|
323           f.read
324         end
325         i = {}
326         history = reader.read(data, i)
327         game = g
328         info = i
329         break
330       rescue ParseException
331       end
332     end
333     
334     unless history
335       warn "Could not load file #{url.path}"
336       return
337     end
338     
339     # create game
340     match = Match.new(game)
341     @view.create(:activate => true,
342                   :name => game.class.plugin_name)
343     setup_single_player(match)
344     match.history = history
345     match.add_info(info)
346     match.url = url
347     controller.reset(match)
348   end
349   
350   def save_game_as
351     match = controller.match
352     if match
353       pattern = if match.game.respond_to?(:game_extensions)
354         match.game.game_extensions.map{|ext| "*.#{ext}"}.join(' ')
355       else
356         '*.*'
357       end
358       url = KDE::FileDialog.get_save_url(
359         KDE::Url.new, pattern, self, KDE.i18n("Save game"))
360       match.url = write_game(url)
361     end
362   end
363   
364   def save_game
365     match = controller.match
366     if match
367       if match.url
368         write_game
369       else
370         save_game_as
371       end
372     end
373   end
374   
375   def write_game(url = nil)
376     return if url.is_empty or (not url.path)
377     
378     match = controller.match
379     if match
380       url ||= match.url
381       writer = match.game.game_writer
382       info = match.info
383       info[:players] = info[:players].inject({}) do |res, pl|
384         res[pl.color] = pl.name
385         res
386       end
387       result = writer.write(info, match.history)
388       write_file(url, result)
389     end
390   end
391   
392   def update_game_actions(contr)
393     unplug_action_list(:game_actions)
394     if contr.match
395       game = contr.match.game
396       actions = if game.respond_to?(:actions)
397         game.actions(self, action_collection, contr.policy)
398       else
399         []
400       end
401       plug_action_list(:game_actions, actions)
402     end
403   end
404   
405   def update_title
406     self.caption = @view.current_name
407   end
408   
409   def update_active_actions(contr)
410     if contr == controller
411       contr.active_actions.each do |id, enabled|
412         action = @actions[id]
413         if action
414           action.enabled = enabled
415         end
416       end
417     end
418   end