f0dc22128d854e465bba1c1cfc1c5ca6e7d297c4
[projects/chimara/chimara.git] / player / player.py
1 #!/usr/bin/env python
2
3 import sys
4 import os.path
5 from gi.repository import GObject, Gdk, Gio, Gtk, Chimara
6 import config
7
8 # FIXME: Dummy translation function, for now
9 _ = lambda x: x
10
11 class Player(GObject.GObject):
12         __gtype_name__ = 'ChimaraPlayer'
13         
14         def __init__(self):
15                 super(Player, self).__init__()
16                 
17                 # FIXME: should use the Keyfile backend, but that's not available from
18                 # Python
19                 self.prefs_settings = Gio.Settings('org.chimara-if.player.preferences')
20                 self.state_settings = Gio.Settings('org.chimara-if.player.state')
21                 
22                 builder = Gtk.Builder()
23                 builder.add_from_file('chimara.ui')
24                 self.window = builder.get_object('chimara')
25                 self.aboutwindow = builder.get_object('aboutwindow')
26                 self.prefswindow = builder.get_object('prefswindow')
27                 actiongroup = builder.get_object('actiongroup')
28                 
29                 # Set the default value of the "View/Toolbar" menu item upon creation
30                 # of a new window to the "show-toolbar-default" setting, but bind the
31                 # setting one-way only - we don't want toolbars to disappear suddenly
32                 toolbar_action = builder.get_object('toolbar')
33                 toolbar_action.active = \
34                         self.state_settings.get_boolean('show-toolbar-default')
35                 self.state_settings.bind('show-toolbar-default', toolbar_action,
36                         'active', Gio.SettingsBindFlags.SET)
37                 
38                 filt = Gtk.RecentFilter()
39                 for pattern in ['*.z[1-8]', '*.[zg]lb', '*.[zg]blorb', '*.ulx', '*.blb',
40                         '*.blorb']:
41                         filt.add_pattern(pattern)
42                 recent = builder.get_object('recent')
43                 recent.add_filter(filt)
44                 
45                 uimanager = Gtk.UIManager()
46                 uimanager.add_ui_from_file('chimara.menus')
47                 uimanager.insert_action_group(actiongroup, 0)
48                 menubar = uimanager.get_widget('/menubar')
49                 toolbar = uimanager.get_widget('/toolbar')
50                 toolbar.no_show_all = True
51                 if toolbar_action.active:
52                         toolbar.show()
53                 else:
54                         toolbar.hide()
55                 
56                 # Connect the accelerators
57                 accels = uimanager.get_accel_group()
58                 self.window.add_accel_group(accels)
59                 
60                 self.glk = Chimara.IF()
61                 self.glk.props.ignore_errors = True
62                 self.glk.set_css_from_file('style.css')
63                 
64                 vbox = builder.get_object('vbox')
65                 vbox.pack_end(self.glk, True, True, 0)
66                 vbox.pack_start(menubar, False, False, 0)
67                 vbox.pack_start(toolbar, False, False, 0)
68                 
69                 #builder.connect_signals(self)  # FIXME Segfaults?!
70                 builder.get_object('open').connect('activate', self.on_open_activate)
71                 builder.get_object('restore').connect('activate',
72                         self.on_restore_activate)
73                 builder.get_object('save').connect('activate', self.on_save_activate)
74                 builder.get_object('stop').connect('activate', self.on_stop_activate)
75                 builder.get_object('recent').connect('item-activated',
76                         self.on_recent_item_activated)
77                 builder.get_object('undo').connect('activate', self.on_undo_activate)
78                 builder.get_object('quit').connect('activate', self.on_quit_activate)
79                 builder.get_object('copy').connect('activate', self.on_copy_activate)
80                 builder.get_object('paste').connect('activate', self.on_paste_activate)
81                 builder.get_object('preferences').connect('activate',
82                         self.on_preferences_activate)
83                 builder.get_object('about').connect('activate', self.on_about_activate)
84                 toolbar_action.connect('toggled', self.on_toolbar_toggled)
85                 self.aboutwindow.connect('response', lambda x, *args: x.hide())
86                 self.aboutwindow.connect('delete-event',
87                         lambda x, *args: x.hide_on_delete())
88                 self.window.connect('delete-event', self.on_window_delete_event)
89                 self.prefswindow.connect('response', lambda x, *args: x.hide())
90                 self.prefswindow.connect('delete-event',
91                         lambda x, *args: x.hide_on_delete())
92                 # FIXME Delete to here when above bug is fixed
93                 
94                 self.glk.connect('notify::program-name', self.change_window_title)
95                 self.glk.connect('notify::story-name', self.change_window_title)
96                 
97                 # Create preferences window
98                 # TODO
99                 
100         def change_window_title(self, glk, pspec, data=None):
101                 if glk.props.program_name is None:
102                         title = "Chimara"
103                 elif glk.props.story_name is None:
104                         title = "{interp} - Chimara".format(interp=glk.props.program_name)
105                 else:
106                         title = "{interp} - {story} - Chimara".format(
107                                 interp=glk.props.program_name,
108                                 story=glk.props.story_name)
109                 self.window.props.title = title
110         
111         def on_open_activate(self, action, data=None):
112                 if not self.confirm_open_new_game(): return
113                 
114                 dialog = Gtk.FileChooserDialog(_('Open Game'), self.window,
115                         Gtk.FileChooserAction.OPEN,
116                         (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
117                         Gtk.STOCK_OPEN, Gtk.ResponseType.ACCEPT))
118                 
119                 # Get last opened path
120                 path = _maybe(self.state_settings.get_value('last-open-path'))
121                 if path is not None:
122                         dialog.set_current_folder(path)
123                 
124                 response = dialog.run()
125                 dialog.hide()
126                 if response != Gtk.ResponseType.ACCEPT:
127                         return
128                 
129                 gamefile = dialog.get_file()
130                 self.search_for_graphics_file(gamefile.get_path())
131                 try:
132                         self.glk.run_game_file(gamefile)
133                 except GLib.Error as e:
134                         error_dialog(self.window, _('Could not open game file {filename}: {errmsg}').format(filename=gamefile.get_path(), errmsg=e.message))
135                         return
136                 
137                 path = dialog.get_current_folder()
138                 if path is not None:
139                         self.state_settings.last_open_path = path
140                 
141                 # Add file to recent files list
142                 manager = Gtk.RecentManager.get_default()
143                 uri = gamefile.get_uri()
144                 manager.add_item(uri)
145                 
146                 dialog.destroy()
147
148         def on_recent_item_activated(self, chooser, data=None):
149                 if not self.confirm_open_new_game(): return
150                 
151                 uri = chooser.get_current_uri()
152                 gamefile = Gio.file_new_for_uri(uri)
153                 
154                 self.search_for_graphics_file(gamefile.get_path())
155                 try:
156                         self.glk.run_game_file(gamefile)
157                 except GLib.Error as e:
158                         error_dialog(self.window,
159                                 _('Could not open game file {filename}: {errmsg}').format(
160                                         filename=gamefile.get_basename(),
161                                         errmsg=e.message))
162                         return
163                 
164                 # Add file to recent files list again, this updates it to most recently
165                 # used
166                 manager = Gtk.RecentManager.get_default()
167                 manager.add_item(uri)
168
169         def on_stop_activate(self, action, data=None):
170                 self.glk.stop()
171
172         def on_quit_chimara_activate(self, action, data=None):
173                 Gtk.main_quit()
174
175         def on_copy_activate(self, action, data=None):
176                 focus = self.window.get_focus()
177                 # Call "copy clipboard" on any widget that defines it
178                 if (isinstance(focus, Gtk.Label) 
179                         or isinstance(focus, Gtk.Entry) 
180                         or isinstance(focus, Gtk.TextView)):
181                         focus.emit('copy-clipboard')
182         
183         def on_paste_activate(self, action, data=None):
184                 focus = self.window.get_focus()
185                 # Call "paste clipboard" on any widget that defines it
186                 if isinstance(focus, Gtk.Entry) or isinstance(focus, Gtk.TextView):
187                         focus.emit('paste-clipboard')
188         
189         def on_preferences_activate(self, action, data=None):
190                 self.prefswindow.present()
191         
192         def on_toolbar_toggled(self, action, data=None):
193                 if action.get_active():
194                         self.toolbar.show()
195                 else:
196                         self.toolbar.hide()
197
198         def on_undo_activate(self, action, data=None):
199                 self.glk.feed_line_input('undo')
200
201         def on_save_activate(self, action, data=None):
202                 self.glk.feed_line_input('save')
203
204         def on_restore_activate(self, action, data=None):
205                 self.glk.feed_line_input('restore')
206
207         def on_restart_activate(self, action, data=None):
208                 self.glk.feed_line_input('restart')
209
210         def on_quit_activate(self, action, data=None):
211                 self.glk.feed_line_input('quit')
212
213         def on_about_activate(self, action, data=None):
214                 self.aboutwindow.set_version(config.PACKAGE_VERSION)
215                 self.aboutwindow.present()
216
217         def on_window_delete_event(self, widget, event, data=None):
218                 Gtk.main_quit()
219                 return True
220                 
221         def confirm_open_new_game(self):
222                 """
223                 If a game is running in the Glk widget, warn the user that they will
224                 quit the currently running game if they open a new one. Returns True if
225                 no game         was running. Returns False if the user cancelled. Returns True
226                 and shuts down the running game if the user wishes to continue.
227                 """
228                 if not self.glk.props.running: return True
229
230                 dialog = Gtk.MessageDialog(self.window,
231                         Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
232                         Gtk.MessageType.WARNING, Gtk.ButtonsType.CANCEL,
233                         _("Are you sure you want to open a new game?"))
234                 dialog.format_secondary_text(
235                         _("If you open a new game, you will quit the one you are "
236                         "currently playing."))
237                 dialog.add_button(Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
238                 response = dialog.run()
239                 dialog.hide()
240                 
241                 if response != Gtk.ResponseType.OK:
242                         return False
243                 
244                 self.glk.stop()
245                 self.glk.wait()
246                 return True
247         
248         def search_for_graphics_file(self, filename):
249                 """Internal function: See if there is a corresponding graphics file"""
250                 
251                 # First get the name of the story file
252                 base = os.path.basename(filename)
253                 base_noext = os.path.splitext(base)[0]
254
255                 # Check in the stored resource path, if set
256                 resource_path = _maybe(self.prefs_settings.get_value('resource-path'))
257                 
258                 # Otherwise check in the current directory
259                 if resource_path is None:
260                         resource_path = os.path.dirname(filename)
261                 
262                 blorbfile = os.path.join(resource_path, base_noext + '.blb')
263                 if os.path.exists(blorbfile):
264                         glk.graphics_file = blorbfile
265
266 def _maybe(variant):
267         """Gets a maybe value from a GVariant - not handled in PyGI"""
268         v = variant.get_maybe()
269         if v is None: return None
270         return v.unpack()
271
272 def error_dialog(parent, message):
273         dialog = Gtk.MessageDialog(parent, Gtk.DialogFlags.DESTROY_WITH_PARENT,
274                 Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, message)
275         dialog.run()
276         dialog.destroy()
277
278 if __name__ == '__main__':
279         Gdk.threads_init()
280
281         player = Player()
282         player.window.show_all()
283
284         if len(sys.argv) == 3:
285                 player.glk.props.graphics_file = sys.argv[2]
286         if len(sys.argv) >= 2:
287                 try:
288                         player.glk.run_game(sys.argv[1])
289                 except GLib.Error as e:
290                         error_dialog(player.window,
291                                 _("Error starting Glk library: {errmsg}").format(
292                                         errmsg=e.message))
293                         sys.exit(1)
294         
295         Gdk.threads_enter()
296         Gtk.main()
297         Gdk.threads_leave()
298         
299         player.glk.stop()
300         player.glk.wait()
301         
302         sys.exit(0)
303