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