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