1
# -*- coding: utf-8 -*-
2
# -*- coding: utf-8 -*-
4
__author__ = 'Robert Ancell <bob27@users.sourceforge.net>'
5
__license__ = 'GNU General Public License Version 2'
6
__copyright__ = 'Copyright 2005-2006 Robert Ancell'
10
# TODO: Extend base UI classes?
15
from gettext import gettext as _
23
from glchess.defaults import *
25
# Stop PyGTK from catching exceptions
26
os.environ['PYGTK_FATAL_EXCEPTIONS'] = '1'
30
import glchess.chess.board
36
# Mark all windows with our icon
37
gtk.window_set_default_icon_name(ICON_NAME)
39
def loadUIFile(name, root = None):
41
ui.set_translation_domain(DOMAIN)
42
ui.add_from_file(os.path.join(UI_DIR, name))
45
class GLibTimer(glchess.ui.Timer):
49
# FIXME: time.time() is _not_ monotonic so this is not really safe at all...
51
def __init__(self, ui, feedback, duration):
53
self.feedback = feedback
55
self.reportedTickTime = 0
60
self.set(duration * 1000)
62
def set(self, duration):
65
if self.timer is not None:
66
gobject.source_remove(self.timer)
67
if self.tickTimer is not None:
68
gobject.source_remove(self.tickTimer)
69
self.duration = duration
72
# Notified if the second has changed
74
def __consumed(self, now):
75
# Total time - time at last start - time since start
76
if self.startTime is None:
79
return self.consumed + (now - self.startTime)
81
def getRemaining(self):
82
"""Extends ui.Timer"""
83
return self.duration - self.__consumed(int(1000 * time.time()))
86
"""Extends ui.Timer"""
87
if self.timer is None:
90
# Calculate the amount of time to use when restarted
91
self.consumed = self.__consumed(int(1000 * time.time()))
94
gobject.source_remove(self.timer)
95
if self.tickTimer is not None:
96
gobject.source_remove(self.tickTimer)
101
"""Extends ui.Timer"""
102
if self.timer is not None:
105
# Notify when all time runs out
106
self.startTime = int(1000 * time.time())
107
self.timer = gobject.timeout_add(self.duration - self.consumed, self.__expired)
109
# Notify on the next second boundary
110
self.__setSecondTimer(self.startTime)
112
def __setSecondTimer(self, now):
113
"""Set a timer to expire on the next second boundary"""
114
assert(self.tickTimer is None)
116
# Round the remaining time up to the nearest second
117
consumed = self.__consumed(now)
118
t = 1000 * (consumed / 1000 + 1)
119
if t <= self.reportedTickTime:
120
self.tickTime = self.reportedTickTime + 1000
124
# Notify on this time
125
if self.tickTime > self.duration:
126
self.tickTimer = None
128
self.tickTimer = gobject.timeout_add(self.tickTime - consumed, self.__tick)
131
"""Called by GLib main loop"""
132
self.feedback.onTick(0)
133
self.feedback.onExpired()
134
if self.tickTimer is not None:
135
gobject.source_remove(self.tickTimer)
137
self.tickTimer = None
141
"""Called by GLib main loop"""
142
self.reportedTickTime = self.tickTime
143
self.feedback.onTick((self.duration - self.tickTime) / 1000)
144
self.tickTimer = None
145
self.__setSecondTimer(int(1000 * time.time()))
149
"""Extends ui.Timer"""
150
gobject.source_remove(self.timer)
152
class GtkUI(glchess.ui.UI):
156
def __init__(self, feedback):
157
"""Constructor for a GTK+ glChess GUI"""
158
self.feedback = feedback
160
self.__networkGames = {}
161
self.newGameDialog = None
162
self.loadGameDialog = None
163
self.__aboutDialog = None
164
self.__saveGameDialogs = {}
165
self.__joinGameDialogs = []
167
# The time stored for animation
168
self.__lastTime = None
169
self.__animationTimer = None
171
self.__renderGL = False
172
self.openGLInfoPrinted = False
174
self.__attentionCounter = 0
176
self.whiteTimeString = 'ā'
177
self.blackTimeString = 'ā'
179
# Theo window width and height when unmaximised and not fullscreen
182
self.isFullscreen = False
183
self.isMaximised = False
187
self._gui = loadUIFile('glchess.ui')
188
self._gui.connect_signals(self)
190
self.mainWindow = self._gui.get_object('glchess_app')
192
# Create the model for the player types
193
self.__playerModel = gtk.ListStore(str, str, str)
194
iter = self.__playerModel.append()
195
# Translators: Player Type Combo: Player is human controlled
196
self.__playerModel.set(iter, 0, '', 1, 'stock_person', 2, _('Human'))
198
self.__logWindow = log.LogWindow(self._gui.get_object('log_notebook'))
200
# Make preferences dialog
201
self.preferences = dialogs.GtkPreferencesDialog(self)
203
# Balance space on each side of the history combo
204
group = gtk.SizeGroup(gtk.SIZE_GROUP_BOTH)
205
group.add_widget(self.__getWidget('left_nav_box'))
206
group.add_widget(self.__getWidget('right_nav_box'))
208
# History combo displays text data
209
combo = self.__getWidget('history_combo')
210
cell = gtk.CellRendererText()
211
combo.pack_start(cell, False)
212
combo.add_attribute(cell, 'text', 2)
214
self._updateViewButtons()
216
# Watch for config changes
217
for key in ['show_toolbar', 'show_history', 'fullscreen',
218
'show_3d', 'show_3d_smooth', 'piece_style', 'show_comments',
219
'show_numbering', 'show_move_hints', 'width', 'height',
220
'move_format', 'promotion_type', 'board_view',
221
'enable_networking']:
222
glchess.config.watch(key, self.__applyConfig)
226
def watchFileDescriptor(self, fd):
228
self._watches[fd] = gobject.io_add_watch(fd, gobject.IO_IN | gobject.IO_PRI | gobject.IO_HUP | gobject.IO_ERR, self.__readData)
230
def unwatchFileDescriptor(self, fd):
232
gobject.source_remove(self._watches.pop(fd))
234
def writeFileDescriptor(self, fd):
236
gobject.io_add_watch(fd, gobject.IO_OUT, self.__writeData)
238
def addTimer(self, feedback, duration):
240
return GLibTimer(self, feedback, duration)
242
def __timerExpired(self, method):
246
def __readData(self, fd, condition):
247
#print (fd, condition)
248
return self.feedback.onReadFileDescriptor(fd)
250
def __writeData(self, fd, condition):
251
#print (fd, condition)
252
return self.feedback.onWriteFileDescriptor(fd)
254
def addAIEngine(self, name):
255
"""Register an AI engine.
257
'name' is the name of the engine.
258
TODO: difficulty etc etc
260
iter = self.__playerModel.append()
261
self.__playerModel.set(iter, 0, name, 1, 'stock_notebook', 2, name)
263
def setView(self, title, feedback, isPlayable = True):
265
moveFormat = glchess.config.get('move_format')
266
showComments = glchess.config.get('show_comments')
267
self.view = chessview.GtkView(self, feedback, moveFormat = moveFormat, showComments = showComments)
268
self.view.setTitle(title)
269
self.view.isPlayable = isPlayable
270
self.view.viewWidget.setRenderGL(self.__renderGL)
271
viewport = self.__getWidget('game_viewport')
272
child = viewport.get_child()
273
if child is not None:
274
viewport.remove(child)
275
viewport.add(self.view.widget)
277
# Set toolbar/menu buttons to state for this game
278
self._updateViewButtons()
281
if self.view is not None:
282
self.setTimers(self.view.whiteTime, self.view.blackTime)
286
def updateTitle(self):
289
# Set the window title to the name of the game
290
if self.view is not None and len(self.view.title) > 0:
291
if self.view.needsSaving:
292
# Translators: Window title when playing a game that needs saving
293
title = _('Chess - *%(game_name)s') % {'game_name': self.view.title}
295
# Translators: Window title when playing a game that is saved
296
title = _('Chess - %(game_name)s') % {'game_name': self.view.title}
298
# Translators: Window title when not playing a game
300
self.mainWindow.set_title(title)
302
def addLogWindow(self, title, executable, description):
305
return self.__logWindow.addView(title, executable, description)
307
def setTimers(self, whiteTime, blackTime):
310
# Translators: Game Timer Label: Indicates that game has no time limit
311
unlimitedTimeText = _('ā')
313
if whiteTime is None:
314
whiteString = unlimitedTimeText
317
whiteString = '%i:%02i' % (t / 60, t % 60)
318
if blackTime is None:
319
blackString = unlimitedTimeText
322
blackString = '%i:%02i' % (t / 60, t % 60)
324
if whiteString != self.whiteTimeString:
325
self.whiteTimeString = whiteString
326
self._gui.get_object('white_time_label').queue_draw()
327
if blackString != self.blackTimeString:
328
self.blackTimeString = blackString
329
self._gui.get_object('black_time_label').queue_draw()
334
This method will not return.
337
for name in ['show_toolbar', 'show_history', 'show_3d', 'show_3d_smooth',
338
'piece_style', 'show_comments', 'show_numbering', 'show_move_hints',
339
'move_format', 'promotion_type', 'board_view', 'maximised',
340
'enable_networking']:
342
value = glchess.config.get(name)
343
except glchess.config.Error:
346
self.__applyConfig(name, value)
349
self.mainWindow.show()
351
# Apply the fullscreen flag after the window has been shown otherwise
352
# gtk.Window.unfullscreen() stops working if the window is set to fullscreen
353
# before being shown. I haven't been able to reproduce this in the simple
354
# case (GTK+ 2.10.6-0ubuntu3).
355
self.__applyConfig('fullscreen', glchess.config.get('fullscreen'))
361
def reportGameLoaded(self, game):
362
"""Extends glchess.ui.UI"""
363
dialogs.GtkNewGameDialog(self, self.__playerModel, game)
365
def addNetworkDialog(self, feedback):
366
"""Extends glchess.ui.UI"""
367
self.__networkDialog = network.GtkNetworkGameDialog(self, feedback)
368
return self.__networkDialog
370
def addNetworkGame(self, name, game):
371
"""Extends glchess.ui.UI"""
372
self.__networkDialog.addNetworkGame(name, game)
374
def removeNetworkGame(self, game):
375
"""Extends glchess.ui.UI"""
376
self.__networkDialog.removeNetworkGame(game)
378
def requestSave(self, title):
379
"""Extends glchess.ui.UI"""
380
dialog = gtk.MessageDialog(flags = gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
381
type = gtk.MESSAGE_WARNING,
382
message_format = title)
383
# Translators: Save Game Dialog: Notice that game needs saving
384
dialog.format_secondary_text(_("If you don't save the changes to this game will be permanently lost"))
385
# Translators: Save Game Dialog: Discard game button
386
dialog.add_button(_('Close _without saving'), gtk.RESPONSE_OK)
387
dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT)
388
dialog.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_ACCEPT)
390
response = dialog.run()
392
if response == gtk.RESPONSE_ACCEPT:
393
return glchess.ui.SAVE_YES
394
elif response == gtk.RESPONSE_OK:
395
return glchess.ui.SAVE_NO
397
return glchess.ui.SAVE_ABORT
400
"""Extends glchess.ui.UI"""
401
# Save the window size
402
if self.width is not None:
403
glchess.config.set('width', self.width)
404
if self.height is not None:
405
glchess.config.set('height', self.height)
409
def _incAttentionCounter(self, offset):
412
self.__attentionCounter += offset
413
self.__updateAttention()
415
def __updateAttention(self):
418
widget = self.mainWindow
419
widget.set_urgency_hint(self.__attentionCounter != 0 and not widget.is_active())
421
def _on_focus_changed(self, widget, event):
423
self.__updateAttention()
425
def _saveView(self, view, path):
431
error = view.feedback.save(path)
433
if error is not None:
435
self.__saveGameDialogs.pop(view)
441
width = glchess.config.get('width')
442
height = glchess.config.get('height')
443
except glchess.config.Error:
446
self.mainWindow.resize(width, height)
448
def __applyConfig(self, name, value):
451
if name == 'width' or name == 'height':
455
# Show/hide the toolbar
456
if name == 'show_toolbar':
457
toolbar = self.__getWidget('toolbar')
463
elif name == 'enable_networking':
464
menuItem = self.__getWidget('menu_play_online_item')
465
toolbarButton = self.__getWidget('play_online_button')
473
# Show/hide the history
474
elif name == 'show_history':
475
box = self.__getWidget('navigation_box')
482
elif name == 'maximised':
483
window = self.mainWindow
490
elif name == 'fullscreen':
491
window = self.mainWindow
495
window.unfullscreen()
497
# Enable/disable OpenGL rendering
498
elif name == 'show_3d':
499
if value and not chessview.haveGLSupport:
500
# Translators: No 3D Dialog: Title
501
title = _('Unable to enable 3D mode')
502
errors = '\n'.join(chessview.openGLErrors)
503
# Translators: No 3D Dialog: Notification to user that they do not have libraries required to enable 3D.
504
# %(error)s will be replaced with a list of reasons why 3D is not available.
505
description = _("""You are unable to play in 3D mode due to the following problems:
508
Please contact your system administrator to resolve these problems, until then you will be able to play chess in 2D mode.""") % {'errors': errors}
509
dialog = gtk.MessageDialog(type = gtk.MESSAGE_WARNING, message_format = title)
510
dialog.format_secondary_text(description)
511
dialog.add_button(gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE)
514
glchess.config.set('show_3d', False)
516
self.__renderGL = value
517
self.__getWidget('menu_view_3d').set_active(value)
518
self.view.viewWidget.setRenderGL(value)
520
elif name == 'show_3d_smooth':
521
if not chessview.haveGLAccumSupport:
523
self.view.feedback.showSmooth(value)
525
elif name == 'piece_style':
528
elif name == 'show_comments':
529
self.view.setShowComments(value)
531
elif name == 'show_move_hints':
532
self.view.feedback.showMoveHints(value)
534
elif name == 'show_numbering':
535
self.view.feedback.showBoardNumbering(value)
537
elif name == 'move_format':
538
self.view.setMoveFormat(value)
540
elif name == 'promotion_type':
543
elif name == 'board_view':
547
assert(False), 'Unknown config item: %s' % name
549
def startAnimation(self):
550
"""Start the animation callback"""
551
if self.__animationTimer is None:
552
self.__lastTime = time.time()
553
self.__animationTimer = gobject.timeout_add(10, self.__animate)
556
# Get the timestep, if it is less than zero or more than a second
557
# then the system clock was probably changed.
559
step = now - self.__lastTime
564
self.__lastTime = now
567
animating = self.feedback.onAnimate(step)
569
self.__animationTimer = None
574
def __getWidget(self, name):
575
widget = self._gui.get_object(name)
576
assert(widget is not None), 'Unable to find widget: %s' % name
579
def _on_white_time_paint(self, widget, event):
581
self.__drawTime(self.whiteTimeString, widget, (0.0, 0.0, 0.0), (1.0, 1.0, 1.0))
583
def _on_black_time_paint(self, widget, event):
585
self.__drawTime(self.blackTimeString, widget, (1.0, 1.0, 1.0), (0.0, 0.0, 0.0))
587
def __drawTime(self, text, widget, fg, bg):
590
if widget.state == gtk.STATE_INSENSITIVE:
594
context = widget.window.cairo_create()
595
context.set_source_rgba(bg[0], bg[1], bg[2], alpha)
598
(_, _, w, h) = widget.get_allocation()
600
context.set_source_rgba(fg[0], fg[1], fg[2], alpha)
601
context.select_font_face('fixed', cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)
602
context.set_font_size(0.6 * h)
603
(x_bearing, y_bearing, width, height, _, _) = context.text_extents(text)
604
context.move_to((w - width) / 2 - x_bearing, (h - height) / 2 - y_bearing)
605
context.show_text(text)
608
widget.set_size_request(int(width) + 6, -1)
610
def _on_toggle_3d_clicked(self, widget):
612
if widget.get_active():
616
glchess.config.set('show_3d', value)
618
def _on_show_logs_clicked(self, widget):
620
window = self._gui.get_object('log_window')
621
if widget.get_active():
626
def _on_history_combo_changed(self, widget):
628
model = widget.get_model()
629
iter = widget.get_active_iter()
633
# Get the move number
634
moveNumber = model.get_value(iter, 1)
636
if moveNumber == len(model) - 1:
639
# Disable buttons when at the end
640
haveMoves = len(model) > 1
641
for widget in ('first_move_button', 'prev_move_button'):
642
self.__getWidget(widget).set_sensitive(haveMoves and moveNumber != 0)
643
for widget in ('last_move_button', 'next_move_button'):
644
self.__getWidget(widget).set_sensitive(haveMoves and moveNumber != -1)
646
self.view._setMoveNumber(moveNumber)
648
def __selectMoveNumber(self, moveNumber):
651
combo = self.__getWidget('history_combo')
653
# Limit moves to the maximum value
654
maxNumber = len(combo.get_model())
656
# Allow negative indexing
658
moveNumber = maxNumber + moveNumber
661
if moveNumber >= maxNumber:
662
moveNumber = maxNumber - 1
664
combo.set_active(moveNumber)
666
def __selectMoveNumberRelative(self, offset):
669
combo = self.__getWidget('history_combo')
670
selected = combo.get_active()
671
maxNumber = len(combo.get_model())
672
new = selected + offset
675
elif new >= maxNumber:
677
self.__selectMoveNumber(new)
679
def _on_history_start_clicked(self, widget):
681
self.__selectMoveNumber(0)
683
def _on_history_previous_clicked(self, widget):
685
self.__selectMoveNumberRelative(-1)
687
def _on_history_next_clicked(self, widget):
689
self.__selectMoveNumberRelative(1)
691
def _on_history_latest_clicked(self, widget):
693
self.__selectMoveNumber(-1)
695
def _updateViewButtons(self):
698
enable = self.view is not None and self.view.isPlayable
699
for widget in ('menu_save_item', 'menu_save_as_item'):
700
self.__getWidget(widget).set_sensitive(enable)
702
combo = self.__getWidget('history_combo')
703
if self.view is None:
704
if combo.get_model() != None:
705
combo.set_model(None)
707
(model, selected) = self.view._getModel()
708
combo.set_model(model)
710
selected = len(model) + selected
711
combo.set_active(selected)
712
self.__getWidget('navigation_box').set_sensitive(enable)
714
enable = enable and self.view.gameResult is None
715
'''FIXME! for widget in ('menu_resign', 'resign_button', 'menu_claim_draw'):
716
self.__getWidget(widget).set_sensitive(enable)'''
718
def _on_new_game_button_clicked(self, widget):
720
if self.newGameDialog:
721
self.newGameDialog.window.present()
723
self.newGameDialog = dialogs.GtkNewGameDialog(self, self.__playerModel)
725
def _on_join_game_button_clicked(self, widget):
727
self.feedback.onNewNetworkGame()
729
def _on_open_game_button_clicked(self, widget):
731
if self.loadGameDialog:
732
self.loadGameDialog.window.present()
734
self.loadGameDialog = dialogs.GtkLoadGameDialog(self)
736
def _on_save_game_button_clicked(self, widget):
738
if self.view.feedback.getFileName() is not None:
739
self.view.feedback.save()
743
dialog = self.__saveGameDialogs[self.view]
745
dialog = self.__saveGameDialogs[self.view] = dialogs.GtkSaveGameDialog(self, self.view)
746
dialog.window.present()
748
def _on_save_as_game_button_clicked(self, widget):
751
dialog = self.__saveGameDialogs[self.view]
753
dialog = self.__saveGameDialogs[self.view] = dialogs.GtkSaveGameDialog(self, self.view, self.view.feedback.getFileName())
754
dialog.window.present()
756
def _on_undo_move_clicked(self, widget):
758
self.view.feedback.undo()
760
def _on_resign_clicked(self, widget):
762
self.view.feedback.resign()
764
def _on_claim_draw_clicked(self, widget):
766
if self.view.feedback.claimDraw():
769
# Translators: Draw Dialog: Title
770
title = _("Unable to claim draw")
771
# Translators: Draw Dialog: Notify user why they cannot claim draw
772
message = _("""You may claim a draw when:
773
a) The board has been in the same state three times (Three fold repetition)
774
b) Fifty moves have occurred where no pawn has moved and no piece has been captured (50 move rule)""")
776
dialog = gtk.MessageDialog(flags = gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
777
type = gtk.MESSAGE_WARNING,
778
message_format = title)
779
dialog.format_secondary_text(message)
780
dialog.add_button(gtk.STOCK_CLOSE, gtk.RESPONSE_ACCEPT)
784
def _on_preferences_clicked(self, widget):
786
self.preferences.setVisible(True)
788
def _on_help_clicked(self, widget):
791
gtk.show_uri(self.mainWindow.get_screen(), "ghelp:glchess", gtk.get_current_event_time())
792
except gobject.GError, e:
793
# TODO: This should be a pop-up dialog
794
print _('Unable to display help: %s') % str(e)
796
def _on_view_fullscreen_clicked(self, widget):
798
glchess.config.set('fullscreen', True)
800
def _on_view_unfullscreen_clicked(self, widget):
802
glchess.config.set('fullscreen', False)
804
def _on_3d_support_dialog_delete_event(self, widget, event):
806
# Stop the dialog from deleting itself
809
def _on_3d_support_dialog_response(self, widget, responseId):
811
if self.__aboutDialog is not None:
816
def _on_about_clicked(self, widget):
818
if self.__aboutDialog is not None:
821
dialog = self.__aboutDialog = gtk.AboutDialog()
822
dialog.set_transient_for(self.mainWindow)
823
dialog.set_name(APPNAME)
824
dialog.set_version(VERSION)
825
dialog.set_copyright(COPYRIGHT)
826
dialog.set_license(LICENSE[0] + '\n\n' + LICENSE[1] + '\n\n' +LICENSE[2])
827
dialog.set_wrap_license(True)
828
dialog.set_comments(DESCRIPTION)
829
dialog.set_authors(AUTHORS)
830
dialog.set_artists(ARTISTS)
831
dialog.set_translator_credits(_("translator-credits"))
832
dialog.set_website(WEBSITE)
833
dialog.set_website_label(WEBSITE_LABEL)
834
dialog.set_logo_icon_name(ICON_NAME)
835
dialog.connect('response', self._on_glchess_about_dialog_close)
838
def _on_glchess_about_dialog_close(self, widget, event):
840
self.__aboutDialog.destroy()
841
self.__aboutDialog = None
844
def _on_log_window_delete_event(self, widget, event):
846
self._gui.get_object('menu_view_logs').set_active(False)
848
# Stop the event - the window will be closed by the menu event
851
def _on_resize(self, widget, event):
853
if self.isMaximised or self.isFullscreen:
855
self.width = event.width
856
self.height = event.height
858
def _on_window_state_changed(self, widget, event):
860
if event.changed_mask & gtk.gdk.WINDOW_STATE_MAXIMIZED:
861
self.isMaximised = event.new_window_state & gtk.gdk.WINDOW_STATE_MAXIMIZED != 0
862
glchess.config.set('maximised', self.isMaximised)
864
if event.changed_mask & gtk.gdk.WINDOW_STATE_FULLSCREEN:
865
self.isFullscreen = event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN != 0
866
if self.isFullscreen:
867
self._gui.get_object('menu_fullscreen').hide()
868
self._gui.get_object('menu_leave_fullscreen').show()
870
self._gui.get_object('menu_leave_fullscreen').hide()
871
self._gui.get_object('menu_fullscreen').show()
873
def _on_close_window(self, widget, event):
875
self.feedback.onQuit()
878
def _on_menu_quit(self, widget):
880
self.feedback.onQuit()
882
if __name__ == '__main__':