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