source: src/mlx/gui/gui.py@ 244:afe588c772a0

Last change on this file since 244:afe588c772a0 was 241:dea155dd3ac0, checked in by István Váradi <ivaradi@…>, 12 years ago

Added support for calculating speeds in km/h for Soviet aircraft

File size: 48.3 KB
Line 
1# The main file for the GUI
2# -*- coding: utf-8 -*-
3
4from statusicon import StatusIcon
5from statusbar import Statusbar
6from info import FlightInfo
7from update import Updater
8from mlx.gui.common import *
9from mlx.gui.flight import Wizard
10from mlx.gui.monitor import MonitorWindow
11from mlx.gui.weighthelp import WeightHelp
12from mlx.gui.gates import FleetGateStatus
13from mlx.gui.prefs import Preferences
14from mlx.gui.checklist import ChecklistEditor
15from mlx.gui.pirep import PIREPViewer
16
17import mlx.const as const
18import mlx.fs as fs
19import mlx.flight as flight
20import mlx.logger as logger
21import mlx.acft as acft
22import mlx.web as web
23import mlx.singleton as singleton
24from mlx.i18n import xstr, getLanguage
25from mlx.pirep import PIREP
26
27import time
28import threading
29import sys
30import datetime
31import webbrowser
32
33#------------------------------------------------------------------------------
34
35class GUI(fs.ConnectionListener):
36 """The main GUI class."""
37 _authors = [ ("Váradi", "István", "prog_test"),
38 ("Galyassy", "Tamás", "negotiation"),
39 ("Petrovszki", "Gábor", "test"),
40 ("Zsebényi-Loksa", "Gergely", "test"),
41 ("Kurják", "Ákos", "test"),
42 ("Radó", "Iván", "test") ]
43
44 def __init__(self, programDirectory, config):
45 """Construct the GUI."""
46 gobject.threads_init()
47
48 self._programDirectory = programDirectory
49 self.config = config
50 self._connecting = False
51 self._reconnecting = False
52 self._connected = False
53 self._logger = logger.Logger(self)
54 self._flight = None
55 self._simulator = None
56 self._monitoring = False
57
58 self._fleet = None
59
60 self._fleetCallback = None
61
62 self._updatePlaneCallback = None
63 self._updatePlaneTailNumber = None
64 self._updatePlaneStatus = None
65 self._updatePlaneGateNumber = None
66
67 self._stdioLock = threading.Lock()
68 self._stdioText = ""
69 self._stdioStartingLine = True
70
71 self._sendPIREPCallback = None
72
73 self.webHandler = web.Handler()
74 self.webHandler.start()
75
76 self.toRestart = False
77
78 def build(self, iconDirectory):
79 """Build the GUI."""
80
81 self._mainWindow = window = gtk.Window()
82 window.set_title(WINDOW_TITLE_BASE)
83 window.set_icon_from_file(os.path.join(iconDirectory, "logo.ico"))
84 window.set_resizable(False)
85 window.connect("delete-event",
86 lambda a, b: self.hideMainWindow())
87 window.connect("window-state-event", self._handleMainWindowState)
88 accelGroup = gtk.AccelGroup()
89 window.add_accel_group(accelGroup)
90
91 mainVBox = gtk.VBox()
92 window.add(mainVBox)
93
94 self._preferences = Preferences(self)
95 self._checklistEditor = ChecklistEditor(self)
96
97 menuBar = self._buildMenuBar(accelGroup)
98 mainVBox.pack_start(menuBar, False, False, 0)
99
100 self._notebook = gtk.Notebook()
101 mainVBox.pack_start(self._notebook, True, True, 4)
102
103 self._wizard = Wizard(self)
104 label = gtk.Label(xstr("tab_flight"))
105 label.set_use_underline(True)
106 label.set_tooltip_text(xstr("tab_flight_tooltip"))
107 self._notebook.append_page(self._wizard, label)
108
109 self._flightInfo = FlightInfo(self)
110 label = gtk.Label(xstr("tab_flight_info"))
111 label.set_use_underline(True)
112 label.set_tooltip_text(xstr("tab_flight_info_tooltip"))
113 self._notebook.append_page(self._flightInfo, label)
114 self._flightInfo.disable()
115
116 self._weightHelp = WeightHelp(self)
117 label = gtk.Label(xstr("tab_weight_help"))
118 label.set_use_underline(True)
119 label.set_tooltip_text(xstr("tab_weight_help_tooltip"))
120 self._notebook.append_page(self._weightHelp, label)
121
122 (logWidget, self._logView) = self._buildLogWidget()
123 addFaultTag(self._logView.get_buffer())
124 label = gtk.Label(xstr("tab_log"))
125 label.set_use_underline(True)
126 label.set_tooltip_text(xstr("tab_log_tooltip"))
127 self._notebook.append_page(logWidget, label)
128
129 self._fleetGateStatus = FleetGateStatus(self)
130 label = gtk.Label(xstr("tab_gates"))
131 label.set_use_underline(True)
132 label.set_tooltip_text(xstr("tab_gates_tooltip"))
133 self._notebook.append_page(self._fleetGateStatus, label)
134
135 (self._debugLogWidget, self._debugLogView) = self._buildLogWidget()
136 self._debugLogWidget.show_all()
137
138 mainVBox.pack_start(gtk.HSeparator(), False, False, 0)
139
140 self._statusbar = Statusbar()
141 mainVBox.pack_start(self._statusbar, False, False, 0)
142
143 self._notebook.connect("switch-page", self._notebookPageSwitch)
144
145 self._monitorWindow = MonitorWindow(self, iconDirectory)
146 self._monitorWindow.add_accel_group(accelGroup)
147 self._monitorWindowX = None
148 self._monitorWindowY = None
149 self._selfToggling = False
150
151 self._pirepViewer = PIREPViewer(self)
152
153 window.show_all()
154 self._wizard.grabDefault()
155 self._weightHelp.reset()
156 self._weightHelp.disable()
157
158 self._statusIcon = StatusIcon(iconDirectory, self)
159
160 self._busyCursor = gdk.Cursor(gdk.CursorType.WATCH if pygobject
161 else gdk.WATCH)
162
163 self._loadPIREPDialog = None
164 self._lastLoadedPIREP = None
165
166 self._hotkeySetID = None
167 self._pilotHotkeyIndex = None
168 self._checklistHotkeyIndex = None
169
170 self._aboutDialog = None
171
172 @property
173 def mainWindow(self):
174 """Get the main window of the GUI."""
175 return self._mainWindow
176
177 @property
178 def logger(self):
179 """Get the logger used by us."""
180 return self._logger
181
182 @property
183 def simulator(self):
184 """Get the simulator used by us."""
185 return self._simulator
186
187 @property
188 def flight(self):
189 """Get the flight being performed."""
190 return self._flight
191
192 @property
193 def entranceExam(self):
194 """Get whether an entrance exam is about to be taken."""
195 return self._wizard.entranceExam
196
197 @property
198 def loggedIn(self):
199 """Indicate if the user has logged in properly."""
200 return self._wizard.loggedIn
201
202 @property
203 def loginResult(self):
204 """Get the result of the login."""
205 return self._wizard.loginResult
206
207 @property
208 def bookedFlight(self):
209 """Get the booked flight selected, if any."""
210 return self._wizard.bookedFlight
211
212 @property
213 def cargoWeight(self):
214 """Get the cargo weight."""
215 return self._wizard.cargoWeight
216
217 @property
218 def zfw(self):
219 """Get Zero-Fuel Weight calculated for the current flight."""
220 return self._wizard.zfw
221
222 @property
223 def filedCruiseAltitude(self):
224 """Get cruise altitude filed for the current flight."""
225 return self._wizard.filedCruiseAltitude
226
227 @property
228 def cruiseAltitude(self):
229 """Get cruise altitude set for the current flight."""
230 return self._wizard.cruiseAltitude
231
232 @property
233 def route(self):
234 """Get the flight route."""
235 return self._wizard.route
236
237 @property
238 def departureMETAR(self):
239 """Get the METAR of the deprature airport."""
240 return self._wizard.departureMETAR
241
242 @property
243 def arrivalMETAR(self):
244 """Get the METAR of the deprature airport."""
245 return self._wizard.arrivalMETAR
246
247 @property
248 def departureRunway(self):
249 """Get the name of the departure runway."""
250 return self._wizard.departureRunway
251
252 @property
253 def sid(self):
254 """Get the SID."""
255 return self._wizard.sid
256
257 @property
258 def v1(self):
259 """Get the V1 speed calculated for the flight."""
260 return self._wizard.v1
261
262 @property
263 def vr(self):
264 """Get the Vr speed calculated for the flight."""
265 return self._wizard.vr
266
267 @property
268 def v2(self):
269 """Get the V2 speed calculated for the flight."""
270 return self._wizard.v2
271
272 @property
273 def arrivalRunway(self):
274 """Get the arrival runway."""
275 return self._wizard.arrivalRunway
276
277 @property
278 def star(self):
279 """Get the STAR."""
280 return self._wizard.star
281
282 @property
283 def transition(self):
284 """Get the transition."""
285 return self._wizard.transition
286
287 @property
288 def approachType(self):
289 """Get the approach type."""
290 return self._wizard.approachType
291
292 @property
293 def vref(self):
294 """Get the Vref speed calculated for the flight."""
295 return self._wizard.vref
296
297 @property
298 def flightType(self):
299 """Get the flight type."""
300 return self._wizard.flightType
301
302 @property
303 def online(self):
304 """Get whether the flight was online or not."""
305 return self._wizard.online
306
307 @property
308 def comments(self):
309 """Get the comments."""
310 return self._flightInfo.comments
311
312 @property
313 def flightDefects(self):
314 """Get the flight defects."""
315 return self._flightInfo.flightDefects
316
317 @property
318 def delayCodes(self):
319 """Get the delay codes."""
320 return self._flightInfo.delayCodes
321
322 def run(self):
323 """Run the GUI."""
324 if self.config.autoUpdate:
325 self._updater = Updater(self,
326 self._programDirectory,
327 self.config.updateURL,
328 self._mainWindow)
329 self._updater.start()
330
331 singleton.raiseCallback = self.raiseCallback
332 gtk.main()
333 singleton.raiseCallback = None
334
335 self._disconnect()
336
337 def connected(self, fsType, descriptor):
338 """Called when we have connected to the simulator."""
339 self._connected = True
340 self._logger.untimedMessage("MLX %s connected to the simulator %s" % \
341 (const.VERSION, descriptor))
342 fs.sendMessage(const.MESSAGETYPE_INFORMATION,
343 "Welcome to MAVA Logger X " + const.VERSION)
344 gobject.idle_add(self._handleConnected, fsType, descriptor)
345
346 def _handleConnected(self, fsType, descriptor):
347 """Called when the connection to the simulator has succeeded."""
348 self._statusbar.updateConnection(self._connecting, self._connected)
349 self.endBusy()
350 if not self._reconnecting:
351 self._wizard.connected(fsType, descriptor)
352 self._reconnecting = False
353 self._listenHotkeys()
354
355 def connectionFailed(self):
356 """Called when the connection failed."""
357 self._logger.untimedMessage("Connection to the simulator failed")
358 gobject.idle_add(self._connectionFailed)
359
360 def _connectionFailed(self):
361 """Called when the connection failed."""
362 self.endBusy()
363 self._statusbar.updateConnection(self._connecting, self._connected)
364
365 dialog = gtk.MessageDialog(parent = self._mainWindow,
366 type = MESSAGETYPE_ERROR,
367 message_format = xstr("conn_failed"))
368
369 dialog.set_title(WINDOW_TITLE_BASE)
370 dialog.format_secondary_markup(xstr("conn_failed_sec"))
371
372 dialog.add_button(xstr("button_cancel"), 0)
373 dialog.add_button(xstr("button_tryagain"), 1)
374 dialog.set_default_response(1)
375
376 result = dialog.run()
377 dialog.hide()
378 if result == 1:
379 self.beginBusy(xstr("connect_busy"))
380 self._simulator.reconnect()
381 else:
382 self.reset()
383
384 def disconnected(self):
385 """Called when we have disconnected from the simulator."""
386 self._connected = False
387 self._logger.untimedMessage("Disconnected from the simulator")
388
389 gobject.idle_add(self._disconnected)
390
391 def _disconnected(self):
392 """Called when we have disconnected from the simulator unexpectedly."""
393 self._statusbar.updateConnection(self._connecting, self._connected)
394
395 dialog = gtk.MessageDialog(type = MESSAGETYPE_ERROR,
396 message_format = xstr("conn_broken"),
397 parent = self._mainWindow)
398 dialog.set_title(WINDOW_TITLE_BASE)
399 dialog.format_secondary_markup(xstr("conn_broken_sec"))
400
401 dialog.add_button(xstr("button_cancel"), 0)
402 dialog.add_button(xstr("button_reconnect"), 1)
403 dialog.set_default_response(1)
404
405 result = dialog.run()
406 dialog.hide()
407 if result == 1:
408 self.beginBusy(xstr("connect_busy"))
409 self._reconnecting = True
410 self._simulator.reconnect()
411 else:
412 self.reset()
413
414 def enableFlightInfo(self):
415 """Enable the flight info tab."""
416 self._flightInfo.enable()
417
418 def cancelFlight(self):
419 """Cancel the current file, if the user confirms it."""
420 dialog = gtk.MessageDialog(parent = self._mainWindow,
421 type = MESSAGETYPE_QUESTION,
422 message_format = xstr("cancelFlight_question"))
423
424 dialog.add_button(xstr("button_no"), RESPONSETYPE_NO)
425 dialog.add_button(xstr("button_yes"), RESPONSETYPE_YES)
426
427 dialog.set_title(WINDOW_TITLE_BASE)
428 result = dialog.run()
429 dialog.hide()
430
431 if result==RESPONSETYPE_YES:
432 self.reset()
433
434 def reset(self):
435 """Reset the GUI."""
436 self._disconnect()
437
438 self._flightInfo.reset()
439 self._flightInfo.disable()
440 self.resetFlightStatus()
441
442 self._weightHelp.reset()
443 self._weightHelp.disable()
444 self._notebook.set_current_page(0)
445
446 self._logView.get_buffer().set_text("")
447
448 if self.loggedIn:
449 self._wizard.reloadFlights(self._handleReloadResult)
450 else:
451 self._wizard.reset(None)
452
453 def _handleReloadResult(self, returned, result):
454 """Handle the result of the reloading of the flights."""
455 self._wizard.reset(result if returned and result.loggedIn else None)
456
457 def _disconnect(self, closingMessage = None, duration = 3):
458 """Disconnect from the simulator if connected."""
459 self.stopMonitoring()
460 self._clearHotkeys()
461
462 if self._connected:
463 if closingMessage is None:
464 self._flight.simulator.disconnect()
465 else:
466 fs.sendMessage(const.MESSAGETYPE_ENVIRONMENT,
467 closingMessage, duration,
468 disconnect = True)
469 self._connected = False
470
471 self._connecting = False
472 self._reconnecting = False
473 self._statusbar.updateConnection(False, False)
474 self._weightHelp.disable()
475
476 return True
477
478 def addFlightLogLine(self, timeStr, line, isFault = False):
479 """Write the given message line to the log."""
480 gobject.idle_add(self._writeLog,
481 formatFlightLogLine(timeStr, line),
482 self._logView, isFault)
483
484 def updateFlightLogLine(self, index, timeStr, line):
485 """Update the line with the given index."""
486 gobject.idle_add(self._updateFlightLogLine, index,
487 formatFlightLogLine(timeStr, line))
488
489 def _updateFlightLogLine(self, index, line):
490 """Replace the contents of the given line in the log."""
491 buffer = self._logView.get_buffer()
492 startIter = buffer.get_iter_at_line(index)
493 endIter = buffer.get_iter_at_line(index + 1)
494 buffer.delete(startIter, endIter)
495 buffer.insert(startIter, line)
496 self._logView.scroll_mark_onscreen(buffer.get_insert())
497
498 def check(self, flight, aircraft, logger, oldState, state):
499 """Update the data."""
500 gobject.idle_add(self._monitorWindow.setData, state)
501 gobject.idle_add(self._statusbar.updateTime, state.timestamp)
502
503 def resetFlightStatus(self):
504 """Reset the status of the flight."""
505 self._statusbar.resetFlightStatus()
506 self._statusbar.updateTime()
507 self._statusIcon.resetFlightStatus()
508
509 def setStage(self, stage):
510 """Set the stage of the flight."""
511 gobject.idle_add(self._setStage, stage)
512
513 def _setStage(self, stage):
514 """Set the stage of the flight."""
515 self._statusbar.setStage(stage)
516 self._statusIcon.setStage(stage)
517 self._wizard.setStage(stage)
518 if stage==const.STAGE_END:
519 self._disconnect(closingMessage =
520 "Flight plan closed. Welcome to %s" % \
521 (self.bookedFlight.arrivalICAO,),
522 duration = 5)
523
524 def setRating(self, rating):
525 """Set the rating of the flight."""
526 gobject.idle_add(self._setRating, rating)
527
528 def _setRating(self, rating):
529 """Set the rating of the flight."""
530 self._statusbar.setRating(rating)
531 self._statusIcon.setRating(rating)
532
533 def setNoGo(self, reason):
534 """Set the rating of the flight to No-Go with the given reason."""
535 gobject.idle_add(self._setNoGo, reason)
536
537 def _setNoGo(self, reason):
538 """Set the rating of the flight."""
539 self._statusbar.setNoGo(reason)
540 self._statusIcon.setNoGo(reason)
541
542 def _handleMainWindowState(self, window, event):
543 """Hande a change in the state of the window"""
544 iconified = gdk.WindowState.ICONIFIED if pygobject \
545 else gdk.WINDOW_STATE_ICONIFIED
546 if self.config.hideMinimizedWindow and \
547 (event.changed_mask&iconified)!=0 and \
548 (event.new_window_state&iconified)!=0:
549 self.hideMainWindow(savePosition = False)
550
551 def raiseCallback(self):
552 """Callback for the singleton handling code."""
553 gobject.idle_add(self.raiseMainWindow)
554
555 def raiseMainWindow(self):
556 """SHow the main window if invisible, and raise it."""
557 if not self._mainWindow.get_visible():
558 self.showMainWindow()
559 self._mainWindow.present()
560
561 def hideMainWindow(self, savePosition = True):
562 """Hide the main window and save its position."""
563 if savePosition:
564 (self._mainWindowX, self._mainWindowY) = \
565 self._mainWindow.get_window().get_root_origin()
566 else:
567 self._mainWindowX = self._mainWindowY = None
568 self._mainWindow.hide()
569 self._statusIcon.mainWindowHidden()
570 return True
571
572 def showMainWindow(self):
573 """Show the main window at its former position."""
574 if self._mainWindowX is not None and self._mainWindowY is not None:
575 self._mainWindow.move(self._mainWindowX, self._mainWindowY)
576
577 self._mainWindow.show()
578 self._mainWindow.deiconify()
579
580 self._statusIcon.mainWindowShown()
581
582 def toggleMainWindow(self):
583 """Toggle the main window."""
584 if self._mainWindow.get_visible():
585 self.hideMainWindow()
586 else:
587 self.showMainWindow()
588
589 def hideMonitorWindow(self, savePosition = True):
590 """Hide the monitor window."""
591 if savePosition:
592 (self._monitorWindowX, self._monitorWindowY) = \
593 self._monitorWindow.get_window().get_root_origin()
594 else:
595 self._monitorWindowX = self._monitorWindowY = None
596 self._monitorWindow.hide()
597 self._statusIcon.monitorWindowHidden()
598 if self._showMonitorMenuItem.get_active():
599 self._selfToggling = True
600 self._showMonitorMenuItem.set_active(False)
601 return True
602
603 def showMonitorWindow(self):
604 """Show the monitor window."""
605 if self._monitorWindowX is not None and self._monitorWindowY is not None:
606 self._monitorWindow.move(self._monitorWindowX, self._monitorWindowY)
607 self._monitorWindow.show_all()
608 self._statusIcon.monitorWindowShown()
609 if not self._showMonitorMenuItem.get_active():
610 self._selfToggling = True
611 self._showMonitorMenuItem.set_active(True)
612
613 def _toggleMonitorWindow(self, menuItem):
614 if self._selfToggling:
615 self._selfToggling = False
616 elif self._monitorWindow.get_visible():
617 self.hideMonitorWindow()
618 else:
619 self.showMonitorWindow()
620
621 def restart(self):
622 """Quit and restart the application."""
623 self.toRestart = True
624 self._quit(force = True)
625
626 def flushStdIO(self):
627 """Flush any text to the standard error that could not be logged."""
628 if self._stdioText:
629 sys.__stderr__.write(self._stdioText)
630
631 def writeStdIO(self, text):
632 """Write the given text into standard I/O log."""
633 with self._stdioLock:
634 self._stdioText += text
635
636 gobject.idle_add(self._writeStdIO)
637
638 def beginBusy(self, message):
639 """Begin a period of background processing."""
640 self._wizard.set_sensitive(False)
641 self._weightHelp.set_sensitive(False)
642 self._mainWindow.get_window().set_cursor(self._busyCursor)
643 self._statusbar.updateBusyState(message)
644
645 def endBusy(self):
646 """End a period of background processing."""
647 self._mainWindow.get_window().set_cursor(None)
648 self._weightHelp.set_sensitive(True)
649 self._wizard.set_sensitive(True)
650 self._statusbar.updateBusyState(None)
651
652 def initializeWeightHelp(self):
653 """Initialize the weight help tab."""
654 self._weightHelp.reset()
655 self._weightHelp.enable()
656
657 def getFleetAsync(self, callback = None, force = None):
658 """Get the fleet asynchronously."""
659 gobject.idle_add(self.getFleet, callback, force)
660
661 def getFleet(self, callback = None, force = False):
662 """Get the fleet.
663
664 If force is False, and we already have a fleet retrieved,
665 that one will be used."""
666 if self._fleet is None or force:
667 self._fleetCallback = callback
668 self.beginBusy(xstr("fleet_busy"))
669 self.webHandler.getFleet(self._fleetResultCallback)
670 else:
671 callback(self._fleet)
672
673 def _fleetResultCallback(self, returned, result):
674 """Called when the fleet has been queried."""
675 gobject.idle_add(self._handleFleetResult, returned, result)
676
677 def _handleFleetResult(self, returned, result):
678 """Handle the fleet result."""
679 self.endBusy()
680 if returned:
681 self._fleet = result.fleet
682 else:
683 self._fleet = None
684
685 dialog = gtk.MessageDialog(parent = self.mainWindow,
686 type = MESSAGETYPE_ERROR,
687 message_format = xstr("fleet_failed"))
688 dialog.add_button(xstr("button_ok"), RESPONSETYPE_OK)
689 dialog.set_title(WINDOW_TITLE_BASE)
690 dialog.run()
691 dialog.hide()
692
693 callback = self._fleetCallback
694 self._fleetCallback = None
695 if callback is not None:
696 callback(self._fleet)
697 self._fleetGateStatus.handleFleet(self._fleet)
698
699 def updatePlane(self, tailNumber, status,
700 gateNumber = None, callback = None):
701 """Update the status of the given plane."""
702 self.beginBusy(xstr("fleet_update_busy"))
703
704 self._updatePlaneCallback = callback
705
706 self._updatePlaneTailNumber = tailNumber
707 self._updatePlaneStatus = status
708 self._updatePlaneGateNumber = gateNumber
709
710 self.webHandler.updatePlane(self._updatePlaneResultCallback,
711 tailNumber, status, gateNumber)
712
713 def _updatePlaneResultCallback(self, returned, result):
714 """Called when the status of a plane has been updated."""
715 gobject.idle_add(self._handleUpdatePlaneResult, returned, result)
716
717 def _handleUpdatePlaneResult(self, returned, result):
718 """Handle the plane update result."""
719 self.endBusy()
720 if returned:
721 success = result.success
722 if success:
723 if self._fleet is not None:
724 self._fleet.updatePlane(self._updatePlaneTailNumber,
725 self._updatePlaneStatus,
726 self._updatePlaneGateNumber)
727 self._fleetGateStatus.handleFleet(self._fleet)
728 else:
729 dialog = gtk.MessageDialog(parent = self.mainWindow,
730 type = MESSAGETYPE_ERROR,
731 message_format = xstr("fleet_update_failed"))
732 dialog.add_button(xstr("button_ok"), RESPONSETYPE_ACCEPT)
733 dialog.set_title(WINDOW_TITLE_BASE)
734 dialog.run()
735 dialog.hide()
736
737 success = None
738
739 callback = self._updatePlaneCallback
740 self._updatePlaneCallback = None
741 if callback is not None:
742 callback(success)
743
744 def _writeStdIO(self):
745 """Perform the real writing."""
746 with self._stdioLock:
747 text = self._stdioText
748 self._stdioText = ""
749 if not text: return
750
751 lines = text.splitlines()
752 if text[-1]=="\n":
753 text = ""
754 else:
755 text = lines[-1]
756 lines = lines[:-1]
757
758 now = datetime.datetime.now()
759 timeStr = "%02d:%02d:%02d: " % (now.hour, now.minute, now.second)
760
761 for line in lines:
762 #print >> sys.__stdout__, line
763 if self._stdioStartingLine:
764 self._writeLog(timeStr, self._debugLogView)
765 self._writeLog(line + "\n", self._debugLogView)
766 self._stdioStartingLine = True
767
768 if text:
769 #print >> sys.__stdout__, text,
770 if self._stdioStartingLine:
771 self._writeLog(timeStr, self._debugLogView)
772 self._writeLog(text, self._debugLogView)
773 self._stdioStartingLine = False
774
775 def connectSimulator(self, aircraftType):
776 """Connect to the simulator for the first time."""
777 self._logger.reset()
778
779 self._flight = flight.Flight(self._logger, self)
780 self._flight.flareTimeFromFS = self.config.flareTimeFromFS
781 self._flight.aircraftType = aircraftType
782 self._flight.aircraft = acft.Aircraft.create(self._flight)
783 self._flight.aircraft._checkers.append(self)
784
785 if self._simulator is None:
786 self._simulator = fs.createSimulator(const.SIM_MSFS9, self)
787 fs.setupMessageSending(self.config, self._simulator)
788 self._setupTimeSync()
789
790 self._flight.simulator = self._simulator
791
792 self.beginBusy(xstr("connect_busy"))
793 self._statusbar.updateConnection(self._connecting, self._connected)
794
795 self._connecting = True
796 self._simulator.connect(self._flight.aircraft)
797
798 def startMonitoring(self):
799 """Start monitoring."""
800 if not self._monitoring:
801 self.simulator.startMonitoring()
802 self._monitoring = True
803
804 def stopMonitoring(self):
805 """Stop monitoring."""
806 if self._monitoring:
807 self.simulator.stopMonitoring()
808 self._monitoring = False
809
810 def _buildMenuBar(self, accelGroup):
811 """Build the main menu bar."""
812 menuBar = gtk.MenuBar()
813
814 fileMenuItem = gtk.MenuItem(xstr("menu_file"))
815 fileMenu = gtk.Menu()
816 fileMenuItem.set_submenu(fileMenu)
817 menuBar.append(fileMenuItem)
818
819 loadPIREPMenuItem = gtk.ImageMenuItem(gtk.STOCK_OPEN)
820 loadPIREPMenuItem.set_use_stock(True)
821 loadPIREPMenuItem.set_label(xstr("menu_file_loadPIREP"))
822 loadPIREPMenuItem.add_accelerator("activate", accelGroup,
823 ord(xstr("menu_file_loadPIREP_key")),
824 CONTROL_MASK, ACCEL_VISIBLE)
825 loadPIREPMenuItem.connect("activate", self._loadPIREP)
826 fileMenu.append(loadPIREPMenuItem)
827
828 fileMenu.append(gtk.SeparatorMenuItem())
829
830 quitMenuItem = gtk.ImageMenuItem(gtk.STOCK_QUIT)
831 quitMenuItem.set_use_stock(True)
832 quitMenuItem.set_label(xstr("menu_file_quit"))
833 quitMenuItem.add_accelerator("activate", accelGroup,
834 ord(xstr("menu_file_quit_key")),
835 CONTROL_MASK, ACCEL_VISIBLE)
836 quitMenuItem.connect("activate", self._quit)
837 fileMenu.append(quitMenuItem)
838
839 toolsMenuItem = gtk.MenuItem(xstr("menu_tools"))
840 toolsMenu = gtk.Menu()
841 toolsMenuItem.set_submenu(toolsMenu)
842 menuBar.append(toolsMenuItem)
843
844 checklistMenuItem = gtk.ImageMenuItem(gtk.STOCK_APPLY)
845 checklistMenuItem.set_use_stock(True)
846 checklistMenuItem.set_label(xstr("menu_tools_chklst"))
847 checklistMenuItem.add_accelerator("activate", accelGroup,
848 ord(xstr("menu_tools_chklst_key")),
849 CONTROL_MASK, ACCEL_VISIBLE)
850 checklistMenuItem.connect("activate", self._editChecklist)
851 toolsMenu.append(checklistMenuItem)
852
853 prefsMenuItem = gtk.ImageMenuItem(gtk.STOCK_PREFERENCES)
854 prefsMenuItem.set_use_stock(True)
855 prefsMenuItem.set_label(xstr("menu_tools_prefs"))
856 prefsMenuItem.add_accelerator("activate", accelGroup,
857 ord(xstr("menu_tools_prefs_key")),
858 CONTROL_MASK, ACCEL_VISIBLE)
859 prefsMenuItem.connect("activate", self._editPreferences)
860 toolsMenu.append(prefsMenuItem)
861
862 viewMenuItem = gtk.MenuItem(xstr("menu_view"))
863 viewMenu = gtk.Menu()
864 viewMenuItem.set_submenu(viewMenu)
865 menuBar.append(viewMenuItem)
866
867 self._showMonitorMenuItem = gtk.CheckMenuItem()
868 self._showMonitorMenuItem.set_label(xstr("menu_view_monitor"))
869 self._showMonitorMenuItem.set_use_underline(True)
870 self._showMonitorMenuItem.set_active(False)
871 self._showMonitorMenuItem.add_accelerator("activate", accelGroup,
872 ord(xstr("menu_view_monitor_key")),
873 CONTROL_MASK, ACCEL_VISIBLE)
874 self._showMonitorMenuItem.connect("toggled", self._toggleMonitorWindow)
875 viewMenu.append(self._showMonitorMenuItem)
876
877 showDebugMenuItem = gtk.CheckMenuItem()
878 showDebugMenuItem.set_label(xstr("menu_view_debug"))
879 showDebugMenuItem.set_use_underline(True)
880 showDebugMenuItem.set_active(False)
881 showDebugMenuItem.add_accelerator("activate", accelGroup,
882 ord(xstr("menu_view_debug_key")),
883 CONTROL_MASK, ACCEL_VISIBLE)
884 showDebugMenuItem.connect("toggled", self._toggleDebugLog)
885 viewMenu.append(showDebugMenuItem)
886
887 helpMenuItem = gtk.MenuItem(xstr("menu_help"))
888 helpMenu = gtk.Menu()
889 helpMenuItem.set_submenu(helpMenu)
890 menuBar.append(helpMenuItem)
891
892 manualMenuItem = gtk.ImageMenuItem(gtk.STOCK_HELP)
893 manualMenuItem.set_use_stock(True)
894 manualMenuItem.set_label(xstr("menu_help_manual"))
895 manualMenuItem.add_accelerator("activate", accelGroup,
896 ord(xstr("menu_help_manual_key")),
897 CONTROL_MASK, ACCEL_VISIBLE)
898 manualMenuItem.connect("activate", self._showManual)
899 helpMenu.append(manualMenuItem)
900
901 helpMenu.append(gtk.SeparatorMenuItem())
902
903 aboutMenuItem = gtk.ImageMenuItem(gtk.STOCK_ABOUT)
904 aboutMenuItem.set_use_stock(True)
905 aboutMenuItem.set_label(xstr("menu_help_about"))
906 aboutMenuItem.add_accelerator("activate", accelGroup,
907 ord(xstr("menu_help_about_key")),
908 CONTROL_MASK, ACCEL_VISIBLE)
909 aboutMenuItem.connect("activate", self._showAbout)
910 helpMenu.append(aboutMenuItem)
911
912 return menuBar
913
914 def _toggleDebugLog(self, menuItem):
915 """Toggle the debug log."""
916 if menuItem.get_active():
917 label = gtk.Label(xstr("tab_debug_log"))
918 label.set_use_underline(True)
919 label.set_tooltip_text(xstr("tab_debug_log_tooltip"))
920 self._debugLogPage = self._notebook.append_page(self._debugLogWidget, label)
921 self._notebook.set_current_page(self._debugLogPage)
922 else:
923 self._notebook.remove_page(self._debugLogPage)
924
925 def _buildLogWidget(self):
926 """Build the widget for the log."""
927 alignment = gtk.Alignment(xscale = 1.0, yscale = 1.0)
928
929 alignment.set_padding(padding_top = 8, padding_bottom = 8,
930 padding_left = 16, padding_right = 16)
931
932 logScroller = gtk.ScrolledWindow()
933 # FIXME: these should be constants in common
934 logScroller.set_policy(gtk.PolicyType.AUTOMATIC if pygobject
935 else gtk.POLICY_AUTOMATIC,
936 gtk.PolicyType.AUTOMATIC if pygobject
937 else gtk.POLICY_AUTOMATIC)
938 logScroller.set_shadow_type(gtk.ShadowType.IN if pygobject
939 else gtk.SHADOW_IN)
940 logView = gtk.TextView()
941 logView.set_editable(False)
942 logView.set_cursor_visible(False)
943 logScroller.add(logView)
944
945 logBox = gtk.VBox()
946 logBox.pack_start(logScroller, True, True, 0)
947 logBox.set_size_request(-1, 200)
948
949 alignment.add(logBox)
950
951 return (alignment, logView)
952
953 def _writeLog(self, msg, logView, isFault = False):
954 """Write the given message to the log."""
955 buffer = logView.get_buffer()
956 appendTextBuffer(buffer, msg, isFault = isFault)
957 logView.scroll_mark_onscreen(buffer.get_insert())
958
959 def _quit(self, what = None, force = False):
960 """Quit from the application."""
961 if force:
962 result=RESPONSETYPE_YES
963 else:
964 dialog = gtk.MessageDialog(parent = self._mainWindow,
965 type = MESSAGETYPE_QUESTION,
966 message_format = xstr("quit_question"))
967
968 dialog.add_button(xstr("button_no"), RESPONSETYPE_NO)
969 dialog.add_button(xstr("button_yes"), RESPONSETYPE_YES)
970
971 dialog.set_title(WINDOW_TITLE_BASE)
972 result = dialog.run()
973 dialog.hide()
974
975 if result==RESPONSETYPE_YES:
976 self._statusIcon.destroy()
977 return gtk.main_quit()
978
979 def _notebookPageSwitch(self, notebook, page, page_num):
980 """Called when the current page of the notebook has changed."""
981 if page_num==0:
982 gobject.idle_add(self._wizard.grabDefault)
983 else:
984 self._mainWindow.set_default(None)
985
986 def _editChecklist(self, menuItem):
987 """Callback for editing the checklists."""
988 self._checklistEditor.run()
989
990 def _editPreferences(self, menuItem):
991 """Callback for editing the preferences."""
992 self._clearHotkeys()
993 self._preferences.run(self.config)
994 self._setupTimeSync()
995 self._listenHotkeys()
996
997 def _setupTimeSync(self):
998 """Enable or disable the simulator time synchronization based on the
999 configuration."""
1000 simulator = self._simulator
1001 if simulator is not None:
1002 if self.config.syncFSTime:
1003 simulator.enableTimeSync()
1004 else:
1005 simulator.disableTimeSync()
1006
1007 def _loadPIREP(self, menuItem):
1008 """Load a PIREP for sending."""
1009 dialog = self._getLoadPirepDialog()
1010
1011 if self._lastLoadedPIREP:
1012 dialog.set_current_folder(os.path.dirname(self._lastLoadedPIREP))
1013 else:
1014 pirepDirectory = self.config.pirepDirectory
1015 if pirepDirectory is not None:
1016 dialog.set_current_folder(pirepDirectory)
1017
1018 result = dialog.run()
1019 dialog.hide()
1020
1021 if result==RESPONSETYPE_OK:
1022 self._lastLoadedPIREP = text2unicode(dialog.get_filename())
1023
1024 pirep = PIREP.load(self._lastLoadedPIREP)
1025 if pirep is None:
1026 dialog = gtk.MessageDialog(parent = self._mainWindow,
1027 type = MESSAGETYPE_ERROR,
1028 message_format = xstr("loadPIREP_failed"))
1029 dialog.add_button(xstr("button_ok"), RESPONSETYPE_OK)
1030 dialog.set_title(WINDOW_TITLE_BASE)
1031 dialog.format_secondary_markup(xstr("loadPIREP_failed_sec"))
1032 dialog.run()
1033 dialog.hide()
1034 else:
1035 dialog = self._getSendLoadedDialog(pirep)
1036 dialog.show_all()
1037 while True:
1038 result = dialog.run()
1039
1040 if result==RESPONSETYPE_OK:
1041 self.sendPIREP(pirep)
1042 elif result==1:
1043 self._pirepViewer.setPIREP(pirep)
1044 self._pirepViewer.show_all()
1045 self._pirepViewer.run()
1046 self._pirepViewer.hide()
1047 else:
1048 break
1049
1050 dialog.hide()
1051
1052 def _getLoadPirepDialog(self):
1053 """Get the PIREP loading file chooser dialog.
1054
1055 If it is not created yet, it will be created."""
1056 if self._loadPIREPDialog is None:
1057 dialog = gtk.FileChooserDialog(title = WINDOW_TITLE_BASE + " - " +
1058 xstr("loadPIREP_browser_title"),
1059 action = FILE_CHOOSER_ACTION_OPEN,
1060 buttons = (gtk.STOCK_CANCEL,
1061 RESPONSETYPE_CANCEL,
1062 gtk.STOCK_OK, RESPONSETYPE_OK),
1063 parent = self._mainWindow)
1064 dialog.set_modal(True)
1065
1066
1067 filter = gtk.FileFilter()
1068 filter.set_name(xstr("file_filter_pireps"))
1069 filter.add_pattern("*.pirep")
1070 dialog.add_filter(filter)
1071
1072 filter = gtk.FileFilter()
1073 filter.set_name(xstr("file_filter_all"))
1074 filter.add_pattern("*.*")
1075 dialog.add_filter(filter)
1076
1077 self._loadPIREPDialog = dialog
1078
1079 return self._loadPIREPDialog
1080
1081 def _getSendLoadedDialog(self, pirep):
1082 """Get a dialog displaying the main information of the flight from the
1083 PIREP and providing Cancel and Send buttons."""
1084 dialog = gtk.Dialog(title = WINDOW_TITLE_BASE + " - " +
1085 xstr("loadPIREP_send_title"),
1086 parent = self._mainWindow,
1087 flags = DIALOG_MODAL)
1088
1089 contentArea = dialog.get_content_area()
1090
1091 label = gtk.Label(xstr("loadPIREP_send_help"))
1092 alignment = gtk.Alignment(xalign = 0.5, yalign = 0.5,
1093 xscale = 0.0, yscale = 0.0)
1094 alignment.set_padding(padding_top = 16, padding_bottom = 0,
1095 padding_left = 48, padding_right = 48)
1096 alignment.add(label)
1097 contentArea.pack_start(alignment, False, False, 8)
1098
1099 table = gtk.Table(5, 2)
1100 tableAlignment = gtk.Alignment(xalign = 0.5, yalign = 0.5,
1101 xscale = 0.0, yscale = 0.0)
1102 tableAlignment.set_padding(padding_top = 0, padding_bottom = 32,
1103 padding_left = 48, padding_right = 48)
1104 table.set_row_spacings(4)
1105 table.set_col_spacings(16)
1106 tableAlignment.add(table)
1107 contentArea.pack_start(tableAlignment, True, True, 8)
1108
1109 bookedFlight = pirep.bookedFlight
1110
1111 label = gtk.Label("<b>" + xstr("loadPIREP_send_flightno") + "</b>")
1112 label.set_use_markup(True)
1113 labelAlignment = gtk.Alignment(xalign = 1.0, yalign = 0.5,
1114 xscale = 0.0, yscale = 0.0)
1115 labelAlignment.add(label)
1116 table.attach(labelAlignment, 0, 1, 0, 1)
1117
1118 label = gtk.Label(bookedFlight.callsign)
1119 labelAlignment = gtk.Alignment(xalign = 0.0, yalign = 0.5,
1120 xscale = 0.0, yscale = 0.0)
1121 labelAlignment.add(label)
1122 table.attach(labelAlignment, 1, 2, 0, 1)
1123
1124 label = gtk.Label("<b>" + xstr("loadPIREP_send_date") + "</b>")
1125 label.set_use_markup(True)
1126 labelAlignment = gtk.Alignment(xalign = 1.0, yalign = 0.5,
1127 xscale = 0.0, yscale = 0.0)
1128 labelAlignment.add(label)
1129 table.attach(labelAlignment, 0, 1, 1, 2)
1130
1131 label = gtk.Label(str(bookedFlight.departureTime.date()))
1132 labelAlignment = gtk.Alignment(xalign = 0.0, yalign = 0.5,
1133 xscale = 0.0, yscale = 0.0)
1134 labelAlignment.add(label)
1135 table.attach(labelAlignment, 1, 2, 1, 2)
1136
1137 label = gtk.Label("<b>" + xstr("loadPIREP_send_from") + "</b>")
1138 label.set_use_markup(True)
1139 labelAlignment = gtk.Alignment(xalign = 1.0, yalign = 0.5,
1140 xscale = 0.0, yscale = 0.0)
1141 labelAlignment.add(label)
1142 table.attach(labelAlignment, 0, 1, 2, 3)
1143
1144 label = gtk.Label(bookedFlight.departureICAO)
1145 labelAlignment = gtk.Alignment(xalign = 0.0, yalign = 0.5,
1146 xscale = 0.0, yscale = 0.0)
1147 labelAlignment.add(label)
1148 table.attach(labelAlignment, 1, 2, 2, 3)
1149
1150 label = gtk.Label("<b>" + xstr("loadPIREP_send_to") + "</b>")
1151 label.set_use_markup(True)
1152 labelAlignment = gtk.Alignment(xalign = 1.0, yalign = 0.5,
1153 xscale = 0.0, yscale = 0.0)
1154 labelAlignment.add(label)
1155 table.attach(labelAlignment, 0, 1, 3, 4)
1156
1157 label = gtk.Label(bookedFlight.arrivalICAO)
1158 labelAlignment = gtk.Alignment(xalign = 0.0, yalign = 0.5,
1159 xscale = 0.0, yscale = 0.0)
1160 labelAlignment.add(label)
1161 table.attach(labelAlignment, 1, 2, 3, 4)
1162
1163 label = gtk.Label("<b>" + xstr("loadPIREP_send_rating") + "</b>")
1164 label.set_use_markup(True)
1165 labelAlignment = gtk.Alignment(xalign = 1.0, yalign = 0.5,
1166 xscale = 0.0, yscale = 0.0)
1167 labelAlignment.add(label)
1168 table.attach(labelAlignment, 0, 1, 4, 5)
1169
1170 rating = pirep.rating
1171 label = gtk.Label()
1172 if rating<0:
1173 label.set_markup('<b><span foreground="red">NO GO</span></b>')
1174 else:
1175 label.set_text("%.1f %%" % (rating,))
1176
1177 labelAlignment = gtk.Alignment(xalign = 0.0, yalign = 0.5,
1178 xscale = 0.0, yscale = 0.0)
1179 labelAlignment.add(label)
1180 table.attach(labelAlignment, 1, 2, 4, 5)
1181
1182 dialog.add_button(xstr("button_cancel"), RESPONSETYPE_REJECT)
1183 dialog.add_button(xstr("viewPIREP"), 1)
1184 dialog.add_button(xstr("sendPIREP"), RESPONSETYPE_OK)
1185
1186 return dialog
1187
1188 def sendPIREP(self, pirep, callback = None):
1189 """Send the given PIREP."""
1190 self.beginBusy(xstr("sendPIREP_busy"))
1191 self._sendPIREPCallback = callback
1192 self.webHandler.sendPIREP(self._pirepSentCallback, pirep)
1193
1194 def _pirepSentCallback(self, returned, result):
1195 """Callback for the PIREP sending result."""
1196 gobject.idle_add(self._handlePIREPSent, returned, result)
1197
1198 def _handlePIREPSent(self, returned, result):
1199 """Callback for the PIREP sending result."""
1200 self.endBusy()
1201 secondaryMarkup = None
1202 type = MESSAGETYPE_ERROR
1203 if returned:
1204 if result.success:
1205 type = MESSAGETYPE_INFO
1206 messageFormat = xstr("sendPIREP_success")
1207 secondaryMarkup = xstr("sendPIREP_success_sec")
1208 elif result.alreadyFlown:
1209 messageFormat = xstr("sendPIREP_already")
1210 secondaryMarkup = xstr("sendPIREP_already_sec")
1211 elif result.notAvailable:
1212 messageFormat = xstr("sendPIREP_notavail")
1213 else:
1214 messageFormat = xstr("sendPIREP_unknown")
1215 secondaryMarkup = xstr("sendPIREP_unknown_sec")
1216 else:
1217 print "PIREP sending failed", result
1218 messageFormat = xstr("sendPIREP_failed")
1219 secondaryMarkup = xstr("sendPIREP_failed_sec")
1220
1221 dialog = gtk.MessageDialog(parent = self._wizard.gui.mainWindow,
1222 type = type, message_format = messageFormat)
1223 dialog.add_button(xstr("button_ok"), RESPONSETYPE_OK)
1224 dialog.set_title(WINDOW_TITLE_BASE)
1225 if secondaryMarkup is not None:
1226 dialog.format_secondary_markup(secondaryMarkup)
1227
1228 dialog.run()
1229 dialog.hide()
1230
1231 callback = self._sendPIREPCallback
1232 self._sendPIREPCallback = None
1233 if callback is not None:
1234 callback(returned, result)
1235
1236 def _listenHotkeys(self):
1237 """Setup the hotkeys based on the configuration."""
1238 if self._hotkeySetID is None and self._simulator is not None:
1239 self._pilotHotkeyIndex = None
1240 self._checklistHotkeyIndex = None
1241
1242 hotkeys = []
1243
1244 config = self.config
1245 if config.enableSounds and config.pilotControlsSounds:
1246 self._pilotHotkeyIndex = len(hotkeys)
1247 hotkeys.append(config.pilotHotkey)
1248
1249 if config.enableChecklists:
1250 self._checklistHotkeyIndex = len(hotkeys)
1251 hotkeys.append(config.checklistHotkey)
1252
1253 if hotkeys:
1254 self._hotkeySetID = \
1255 self._simulator.listenHotkeys(hotkeys, self._handleHotkeys)
1256
1257 def _clearHotkeys(self):
1258 """Clear the hotkeys."""
1259 if self._hotkeySetID is not None:
1260 self._hotkeySetID=None
1261 self._simulator.clearHotkeys()
1262
1263 def _handleHotkeys(self, id, hotkeys):
1264 """Handle the hotkeys."""
1265 if id==self._hotkeySetID:
1266 for index in hotkeys:
1267 if index==self._pilotHotkeyIndex:
1268 print "gui.GUI._handleHotkeys: pilot hotkey pressed"
1269 self._flight.pilotHotkeyPressed()
1270 elif index==self._checklistHotkeyIndex:
1271 print "gui.GUI._handleHotkeys: checklist hotkey pressed"
1272 self._flight.checklistHotkeyPressed()
1273 else:
1274 print "gui.GUI._handleHotkeys: unhandled hotkey index:", index
1275
1276 def _showManual(self, menuitem):
1277 """Show the user's manual."""
1278 webbrowser.open(url ="file://" +
1279 os.path.join(self._programDirectory, "doc", "manual",
1280 getLanguage(), "index.html"),
1281 new = 1)
1282
1283 def _showAbout(self, menuitem):
1284 """Show the about dialog."""
1285 dialog = self._getAboutDialog()
1286 dialog.show_all()
1287 dialog.run()
1288 dialog.hide()
1289
1290 def _getAboutDialog(self):
1291 """Get the about dialog.
1292
1293 If it does not exist yet, it will be created."""
1294 if self._aboutDialog is None:
1295 self._aboutDialog = dialog = gtk.AboutDialog()
1296 dialog.set_transient_for(self._mainWindow)
1297 dialog.set_modal(True)
1298
1299 logoPath = os.path.join(self._programDirectory, "logo.png")
1300 logo = pixbuf_new_from_file(logoPath)
1301 dialog.set_logo(logo)
1302
1303 dialog.set_program_name(PROGRAM_NAME)
1304 dialog.set_version(const.VERSION)
1305 dialog.set_copyright("(c) 2012 by István Váradi")
1306 dialog.set_website("http://mlx.varadiistvan.hu")
1307 dialog.set_website_label(xstr("about_website"))
1308
1309 isHungarian = getLanguage()=="hu"
1310 authors = []
1311 for (familyName, firstName, role) in GUI._authors:
1312 authors.append("%s %s (%s)" % \
1313 (familyName if isHungarian else firstName,
1314 firstName if isHungarian else familyName,
1315 xstr("about_role_" + role)))
1316 dialog.set_authors(authors)
1317
1318 dialog.set_license(xstr("about_license"))
1319
1320 if not pygobject:
1321 gtk.about_dialog_set_url_hook(self._showAboutURL, None)
1322
1323 return self._aboutDialog
1324
1325 def _showAboutURL(self, dialog, link, user_data):
1326 """Show the about URL."""
1327 webbrowser.open(url = link, new = 1)
Note: See TracBrowser for help on using the repository browser.