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