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