source: src/mlx/gui/gui.py@ 239:d105a58c4273

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

The PIREP dialog is not hidden while viewing or sending the PIREP

File size: 48.2 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("Connected to the simulator %s" % (descriptor,))
341 fs.sendMessage(const.MESSAGETYPE_INFORMATION,
342 "Welcome to MAVA Logger X " + const.VERSION)
343 gobject.idle_add(self._handleConnected, fsType, descriptor)
344
345 def _handleConnected(self, fsType, descriptor):
346 """Called when the connection to the simulator has succeeded."""
347 self._statusbar.updateConnection(self._connecting, self._connected)
348 self.endBusy()
349 if not self._reconnecting:
350 self._wizard.connected(fsType, descriptor)
351 self._reconnecting = False
352 self._listenHotkeys()
353
354 def connectionFailed(self):
355 """Called when the connection failed."""
356 self._logger.untimedMessage("Connection to the simulator failed")
357 gobject.idle_add(self._connectionFailed)
358
359 def _connectionFailed(self):
360 """Called when the connection failed."""
361 self.endBusy()
362 self._statusbar.updateConnection(self._connecting, self._connected)
363
364 dialog = gtk.MessageDialog(parent = self._mainWindow,
365 type = MESSAGETYPE_ERROR,
366 message_format = xstr("conn_failed"))
367
368 dialog.set_title(WINDOW_TITLE_BASE)
369 dialog.format_secondary_markup(xstr("conn_failed_sec"))
370
371 dialog.add_button(xstr("button_cancel"), 0)
372 dialog.add_button(xstr("button_tryagain"), 1)
373 dialog.set_default_response(1)
374
375 result = dialog.run()
376 dialog.hide()
377 if result == 1:
378 self.beginBusy(xstr("connect_busy"))
379 self._simulator.reconnect()
380 else:
381 self.reset()
382
383 def disconnected(self):
384 """Called when we have disconnected from the simulator."""
385 self._connected = False
386 self._logger.untimedMessage("Disconnected from the simulator")
387
388 gobject.idle_add(self._disconnected)
389
390 def _disconnected(self):
391 """Called when we have disconnected from the simulator unexpectedly."""
392 self._statusbar.updateConnection(self._connecting, self._connected)
393
394 dialog = gtk.MessageDialog(type = MESSAGETYPE_ERROR,
395 message_format = xstr("conn_broken"),
396 parent = self._mainWindow)
397 dialog.set_title(WINDOW_TITLE_BASE)
398 dialog.format_secondary_markup(xstr("conn_broken_sec"))
399
400 dialog.add_button(xstr("button_cancel"), 0)
401 dialog.add_button(xstr("button_reconnect"), 1)
402 dialog.set_default_response(1)
403
404 result = dialog.run()
405 dialog.hide()
406 if result == 1:
407 self.beginBusy(xstr("connect_busy"))
408 self._reconnecting = True
409 self._simulator.reconnect()
410 else:
411 self.reset()
412
413 def enableFlightInfo(self):
414 """Enable the flight info tab."""
415 self._flightInfo.enable()
416
417 def cancelFlight(self):
418 """Cancel the current file, if the user confirms it."""
419 dialog = gtk.MessageDialog(parent = self._mainWindow,
420 type = MESSAGETYPE_QUESTION,
421 message_format = xstr("cancelFlight_question"))
422
423 dialog.add_button(xstr("button_no"), RESPONSETYPE_NO)
424 dialog.add_button(xstr("button_yes"), RESPONSETYPE_YES)
425
426 dialog.set_title(WINDOW_TITLE_BASE)
427 result = dialog.run()
428 dialog.hide()
429
430 if result==RESPONSETYPE_YES:
431 self.reset()
432
433 def reset(self):
434 """Reset the GUI."""
435 self._disconnect()
436
437 self._flightInfo.reset()
438 self._flightInfo.disable()
439 self.resetFlightStatus()
440
441 self._weightHelp.reset()
442 self._weightHelp.disable()
443 self._notebook.set_current_page(0)
444
445 self._logView.get_buffer().set_text("")
446
447 if self.loggedIn:
448 self._wizard.reloadFlights(self._handleReloadResult)
449 else:
450 self._wizard.reset(None)
451
452 def _handleReloadResult(self, returned, result):
453 """Handle the result of the reloading of the flights."""
454 self._wizard.reset(result if returned and result.loggedIn else None)
455
456 def _disconnect(self, closingMessage = None, duration = 3):
457 """Disconnect from the simulator if connected."""
458 self.stopMonitoring()
459 self._clearHotkeys()
460
461 if self._connected:
462 if closingMessage is None:
463 self._flight.simulator.disconnect()
464 else:
465 fs.sendMessage(const.MESSAGETYPE_ENVIRONMENT,
466 closingMessage, duration,
467 disconnect = True)
468 self._connected = False
469
470 self._connecting = False
471 self._reconnecting = False
472 self._statusbar.updateConnection(False, False)
473 self._weightHelp.disable()
474
475 return True
476
477 def addFlightLogLine(self, timeStr, line, isFault = False):
478 """Write the given message line to the log."""
479 gobject.idle_add(self._writeLog,
480 formatFlightLogLine(timeStr, line),
481 self._logView, isFault)
482
483 def updateFlightLogLine(self, index, timeStr, line):
484 """Update the line with the given index."""
485 gobject.idle_add(self._updateFlightLogLine, index,
486 formatFlightLogLine(timeStr, line))
487
488 def _updateFlightLogLine(self, index, line):
489 """Replace the contents of the given line in the log."""
490 buffer = self._logView.get_buffer()
491 startIter = buffer.get_iter_at_line(index)
492 endIter = buffer.get_iter_at_line(index + 1)
493 buffer.delete(startIter, endIter)
494 buffer.insert(startIter, line)
495 self._logView.scroll_mark_onscreen(buffer.get_insert())
496
497 def check(self, flight, aircraft, logger, oldState, state):
498 """Update the data."""
499 gobject.idle_add(self._monitorWindow.setData, state)
500 gobject.idle_add(self._statusbar.updateTime, state.timestamp)
501
502 def resetFlightStatus(self):
503 """Reset the status of the flight."""
504 self._statusbar.resetFlightStatus()
505 self._statusbar.updateTime()
506 self._statusIcon.resetFlightStatus()
507
508 def setStage(self, stage):
509 """Set the stage of the flight."""
510 gobject.idle_add(self._setStage, stage)
511
512 def _setStage(self, stage):
513 """Set the stage of the flight."""
514 self._statusbar.setStage(stage)
515 self._statusIcon.setStage(stage)
516 self._wizard.setStage(stage)
517 if stage==const.STAGE_END:
518 self._disconnect(closingMessage =
519 "Flight plan closed. Welcome to %s" % \
520 (self.bookedFlight.arrivalICAO,),
521 duration = 5)
522
523 def setRating(self, rating):
524 """Set the rating of the flight."""
525 gobject.idle_add(self._setRating, rating)
526
527 def _setRating(self, rating):
528 """Set the rating of the flight."""
529 self._statusbar.setRating(rating)
530 self._statusIcon.setRating(rating)
531
532 def setNoGo(self, reason):
533 """Set the rating of the flight to No-Go with the given reason."""
534 gobject.idle_add(self._setNoGo, reason)
535
536 def _setNoGo(self, reason):
537 """Set the rating of the flight."""
538 self._statusbar.setNoGo(reason)
539 self._statusIcon.setNoGo(reason)
540
541 def _handleMainWindowState(self, window, event):
542 """Hande a change in the state of the window"""
543 iconified = gdk.WindowState.ICONIFIED if pygobject \
544 else gdk.WINDOW_STATE_ICONIFIED
545 if self.config.hideMinimizedWindow and \
546 (event.changed_mask&iconified)!=0 and \
547 (event.new_window_state&iconified)!=0:
548 self.hideMainWindow(savePosition = False)
549
550 def raiseCallback(self):
551 """Callback for the singleton handling code."""
552 gobject.idle_add(self.raiseMainWindow)
553
554 def raiseMainWindow(self):
555 """SHow the main window if invisible, and raise it."""
556 if not self._mainWindow.get_visible():
557 self.showMainWindow()
558 self._mainWindow.present()
559
560 def hideMainWindow(self, savePosition = True):
561 """Hide the main window and save its position."""
562 if savePosition:
563 (self._mainWindowX, self._mainWindowY) = \
564 self._mainWindow.get_window().get_root_origin()
565 else:
566 self._mainWindowX = self._mainWindowY = None
567 self._mainWindow.hide()
568 self._statusIcon.mainWindowHidden()
569 return True
570
571 def showMainWindow(self):
572 """Show the main window at its former position."""
573 if self._mainWindowX is not None and self._mainWindowY is not None:
574 self._mainWindow.move(self._mainWindowX, self._mainWindowY)
575
576 self._mainWindow.show()
577 self._mainWindow.deiconify()
578
579 self._statusIcon.mainWindowShown()
580
581 def toggleMainWindow(self):
582 """Toggle the main window."""
583 if self._mainWindow.get_visible():
584 self.hideMainWindow()
585 else:
586 self.showMainWindow()
587
588 def hideMonitorWindow(self, savePosition = True):
589 """Hide the monitor window."""
590 if savePosition:
591 (self._monitorWindowX, self._monitorWindowY) = \
592 self._monitorWindow.get_window().get_root_origin()
593 else:
594 self._monitorWindowX = self._monitorWindowY = None
595 self._monitorWindow.hide()
596 self._statusIcon.monitorWindowHidden()
597 if self._showMonitorMenuItem.get_active():
598 self._selfToggling = True
599 self._showMonitorMenuItem.set_active(False)
600 return True
601
602 def showMonitorWindow(self):
603 """Show the monitor window."""
604 if self._monitorWindowX is not None and self._monitorWindowY is not None:
605 self._monitorWindow.move(self._monitorWindowX, self._monitorWindowY)
606 self._monitorWindow.show_all()
607 self._statusIcon.monitorWindowShown()
608 if not self._showMonitorMenuItem.get_active():
609 self._selfToggling = True
610 self._showMonitorMenuItem.set_active(True)
611
612 def _toggleMonitorWindow(self, menuItem):
613 if self._selfToggling:
614 self._selfToggling = False
615 elif self._monitorWindow.get_visible():
616 self.hideMonitorWindow()
617 else:
618 self.showMonitorWindow()
619
620 def restart(self):
621 """Quit and restart the application."""
622 self.toRestart = True
623 self._quit(force = True)
624
625 def flushStdIO(self):
626 """Flush any text to the standard error that could not be logged."""
627 if self._stdioText:
628 sys.__stderr__.write(self._stdioText)
629
630 def writeStdIO(self, text):
631 """Write the given text into standard I/O log."""
632 with self._stdioLock:
633 self._stdioText += text
634
635 gobject.idle_add(self._writeStdIO)
636
637 def beginBusy(self, message):
638 """Begin a period of background processing."""
639 self._wizard.set_sensitive(False)
640 self._weightHelp.set_sensitive(False)
641 self._mainWindow.get_window().set_cursor(self._busyCursor)
642 self._statusbar.updateBusyState(message)
643
644 def endBusy(self):
645 """End a period of background processing."""
646 self._mainWindow.get_window().set_cursor(None)
647 self._weightHelp.set_sensitive(True)
648 self._wizard.set_sensitive(True)
649 self._statusbar.updateBusyState(None)
650
651 def initializeWeightHelp(self):
652 """Initialize the weight help tab."""
653 self._weightHelp.reset()
654 self._weightHelp.enable()
655
656 def getFleetAsync(self, callback = None, force = None):
657 """Get the fleet asynchronously."""
658 gobject.idle_add(self.getFleet, callback, force)
659
660 def getFleet(self, callback = None, force = False):
661 """Get the fleet.
662
663 If force is False, and we already have a fleet retrieved,
664 that one will be used."""
665 if self._fleet is None or force:
666 self._fleetCallback = callback
667 self.beginBusy(xstr("fleet_busy"))
668 self.webHandler.getFleet(self._fleetResultCallback)
669 else:
670 callback(self._fleet)
671
672 def _fleetResultCallback(self, returned, result):
673 """Called when the fleet has been queried."""
674 gobject.idle_add(self._handleFleetResult, returned, result)
675
676 def _handleFleetResult(self, returned, result):
677 """Handle the fleet result."""
678 self.endBusy()
679 if returned:
680 self._fleet = result.fleet
681 else:
682 self._fleet = None
683
684 dialog = gtk.MessageDialog(parent = self.mainWindow,
685 type = MESSAGETYPE_ERROR,
686 message_format = xstr("fleet_failed"))
687 dialog.add_button(xstr("button_ok"), RESPONSETYPE_OK)
688 dialog.set_title(WINDOW_TITLE_BASE)
689 dialog.run()
690 dialog.hide()
691
692 callback = self._fleetCallback
693 self._fleetCallback = None
694 if callback is not None:
695 callback(self._fleet)
696 self._fleetGateStatus.handleFleet(self._fleet)
697
698 def updatePlane(self, tailNumber, status,
699 gateNumber = None, callback = None):
700 """Update the status of the given plane."""
701 self.beginBusy(xstr("fleet_update_busy"))
702
703 self._updatePlaneCallback = callback
704
705 self._updatePlaneTailNumber = tailNumber
706 self._updatePlaneStatus = status
707 self._updatePlaneGateNumber = gateNumber
708
709 self.webHandler.updatePlane(self._updatePlaneResultCallback,
710 tailNumber, status, gateNumber)
711
712 def _updatePlaneResultCallback(self, returned, result):
713 """Called when the status of a plane has been updated."""
714 gobject.idle_add(self._handleUpdatePlaneResult, returned, result)
715
716 def _handleUpdatePlaneResult(self, returned, result):
717 """Handle the plane update result."""
718 self.endBusy()
719 if returned:
720 success = result.success
721 if success:
722 if self._fleet is not None:
723 self._fleet.updatePlane(self._updatePlaneTailNumber,
724 self._updatePlaneStatus,
725 self._updatePlaneGateNumber)
726 self._fleetGateStatus.handleFleet(self._fleet)
727 else:
728 dialog = gtk.MessageDialog(parent = self.mainWindow,
729 type = MESSAGETYPE_ERROR,
730 message_format = xstr("fleet_update_failed"))
731 dialog.add_button(xstr("button_ok"), RESPONSETYPE_ACCEPT)
732 dialog.set_title(WINDOW_TITLE_BASE)
733 dialog.run()
734 dialog.hide()
735
736 success = None
737
738 callback = self._updatePlaneCallback
739 self._updatePlaneCallback = None
740 if callback is not None:
741 callback(success)
742
743 def _writeStdIO(self):
744 """Perform the real writing."""
745 with self._stdioLock:
746 text = self._stdioText
747 self._stdioText = ""
748 if not text: return
749
750 lines = text.splitlines()
751 if text[-1]=="\n":
752 text = ""
753 else:
754 text = lines[-1]
755 lines = lines[:-1]
756
757 now = datetime.datetime.now()
758 timeStr = "%02d:%02d:%02d: " % (now.hour, now.minute, now.second)
759
760 for line in lines:
761 #print >> sys.__stdout__, line
762 if self._stdioStartingLine:
763 self._writeLog(timeStr, self._debugLogView)
764 self._writeLog(line + "\n", self._debugLogView)
765 self._stdioStartingLine = True
766
767 if text:
768 #print >> sys.__stdout__, text,
769 if self._stdioStartingLine:
770 self._writeLog(timeStr, self._debugLogView)
771 self._writeLog(text, self._debugLogView)
772 self._stdioStartingLine = False
773
774 def connectSimulator(self, aircraftType):
775 """Connect to the simulator for the first time."""
776 self._logger.reset()
777
778 self._flight = flight.Flight(self._logger, self)
779 self._flight.flareTimeFromFS = self.config.flareTimeFromFS
780 self._flight.aircraftType = aircraftType
781 self._flight.aircraft = acft.Aircraft.create(self._flight)
782 self._flight.aircraft._checkers.append(self)
783
784 if self._simulator is None:
785 self._simulator = fs.createSimulator(const.SIM_MSFS9, self)
786 fs.setupMessageSending(self.config, self._simulator)
787 self._setupTimeSync()
788
789 self._flight.simulator = self._simulator
790
791 self.beginBusy(xstr("connect_busy"))
792 self._statusbar.updateConnection(self._connecting, self._connected)
793
794 self._connecting = True
795 self._simulator.connect(self._flight.aircraft)
796
797 def startMonitoring(self):
798 """Start monitoring."""
799 if not self._monitoring:
800 self.simulator.startMonitoring()
801 self._monitoring = True
802
803 def stopMonitoring(self):
804 """Stop monitoring."""
805 if self._monitoring:
806 self.simulator.stopMonitoring()
807 self._monitoring = False
808
809 def _buildMenuBar(self, accelGroup):
810 """Build the main menu bar."""
811 menuBar = gtk.MenuBar()
812
813 fileMenuItem = gtk.MenuItem(xstr("menu_file"))
814 fileMenu = gtk.Menu()
815 fileMenuItem.set_submenu(fileMenu)
816 menuBar.append(fileMenuItem)
817
818 loadPIREPMenuItem = gtk.ImageMenuItem(gtk.STOCK_OPEN)
819 loadPIREPMenuItem.set_use_stock(True)
820 loadPIREPMenuItem.set_label(xstr("menu_file_loadPIREP"))
821 loadPIREPMenuItem.add_accelerator("activate", accelGroup,
822 ord(xstr("menu_file_loadPIREP_key")),
823 CONTROL_MASK, ACCEL_VISIBLE)
824 loadPIREPMenuItem.connect("activate", self._loadPIREP)
825 fileMenu.append(loadPIREPMenuItem)
826
827 fileMenu.append(gtk.SeparatorMenuItem())
828
829 quitMenuItem = gtk.ImageMenuItem(gtk.STOCK_QUIT)
830 quitMenuItem.set_use_stock(True)
831 quitMenuItem.set_label(xstr("menu_file_quit"))
832 quitMenuItem.add_accelerator("activate", accelGroup,
833 ord(xstr("menu_file_quit_key")),
834 CONTROL_MASK, ACCEL_VISIBLE)
835 quitMenuItem.connect("activate", self._quit)
836 fileMenu.append(quitMenuItem)
837
838 toolsMenuItem = gtk.MenuItem(xstr("menu_tools"))
839 toolsMenu = gtk.Menu()
840 toolsMenuItem.set_submenu(toolsMenu)
841 menuBar.append(toolsMenuItem)
842
843 checklistMenuItem = gtk.ImageMenuItem(gtk.STOCK_APPLY)
844 checklistMenuItem.set_use_stock(True)
845 checklistMenuItem.set_label(xstr("menu_tools_chklst"))
846 checklistMenuItem.add_accelerator("activate", accelGroup,
847 ord(xstr("menu_tools_chklst_key")),
848 CONTROL_MASK, ACCEL_VISIBLE)
849 checklistMenuItem.connect("activate", self._editChecklist)
850 toolsMenu.append(checklistMenuItem)
851
852 prefsMenuItem = gtk.ImageMenuItem(gtk.STOCK_PREFERENCES)
853 prefsMenuItem.set_use_stock(True)
854 prefsMenuItem.set_label(xstr("menu_tools_prefs"))
855 prefsMenuItem.add_accelerator("activate", accelGroup,
856 ord(xstr("menu_tools_prefs_key")),
857 CONTROL_MASK, ACCEL_VISIBLE)
858 prefsMenuItem.connect("activate", self._editPreferences)
859 toolsMenu.append(prefsMenuItem)
860
861 viewMenuItem = gtk.MenuItem(xstr("menu_view"))
862 viewMenu = gtk.Menu()
863 viewMenuItem.set_submenu(viewMenu)
864 menuBar.append(viewMenuItem)
865
866 self._showMonitorMenuItem = gtk.CheckMenuItem()
867 self._showMonitorMenuItem.set_label(xstr("menu_view_monitor"))
868 self._showMonitorMenuItem.set_use_underline(True)
869 self._showMonitorMenuItem.set_active(False)
870 self._showMonitorMenuItem.add_accelerator("activate", accelGroup,
871 ord(xstr("menu_view_monitor_key")),
872 CONTROL_MASK, ACCEL_VISIBLE)
873 self._showMonitorMenuItem.connect("toggled", self._toggleMonitorWindow)
874 viewMenu.append(self._showMonitorMenuItem)
875
876 showDebugMenuItem = gtk.CheckMenuItem()
877 showDebugMenuItem.set_label(xstr("menu_view_debug"))
878 showDebugMenuItem.set_use_underline(True)
879 showDebugMenuItem.set_active(False)
880 showDebugMenuItem.add_accelerator("activate", accelGroup,
881 ord(xstr("menu_view_debug_key")),
882 CONTROL_MASK, ACCEL_VISIBLE)
883 showDebugMenuItem.connect("toggled", self._toggleDebugLog)
884 viewMenu.append(showDebugMenuItem)
885
886 helpMenuItem = gtk.MenuItem(xstr("menu_help"))
887 helpMenu = gtk.Menu()
888 helpMenuItem.set_submenu(helpMenu)
889 menuBar.append(helpMenuItem)
890
891 manualMenuItem = gtk.ImageMenuItem(gtk.STOCK_HELP)
892 manualMenuItem.set_use_stock(True)
893 manualMenuItem.set_label(xstr("menu_help_manual"))
894 manualMenuItem.add_accelerator("activate", accelGroup,
895 ord(xstr("menu_help_manual_key")),
896 CONTROL_MASK, ACCEL_VISIBLE)
897 manualMenuItem.connect("activate", self._showManual)
898 helpMenu.append(manualMenuItem)
899
900 helpMenu.append(gtk.SeparatorMenuItem())
901
902 aboutMenuItem = gtk.ImageMenuItem(gtk.STOCK_ABOUT)
903 aboutMenuItem.set_use_stock(True)
904 aboutMenuItem.set_label(xstr("menu_help_about"))
905 aboutMenuItem.add_accelerator("activate", accelGroup,
906 ord(xstr("menu_help_about_key")),
907 CONTROL_MASK, ACCEL_VISIBLE)
908 aboutMenuItem.connect("activate", self._showAbout)
909 helpMenu.append(aboutMenuItem)
910
911 return menuBar
912
913 def _toggleDebugLog(self, menuItem):
914 """Toggle the debug log."""
915 if menuItem.get_active():
916 label = gtk.Label(xstr("tab_debug_log"))
917 label.set_use_underline(True)
918 label.set_tooltip_text(xstr("tab_debug_log_tooltip"))
919 self._debugLogPage = self._notebook.append_page(self._debugLogWidget, label)
920 self._notebook.set_current_page(self._debugLogPage)
921 else:
922 self._notebook.remove_page(self._debugLogPage)
923
924 def _buildLogWidget(self):
925 """Build the widget for the log."""
926 alignment = gtk.Alignment(xscale = 1.0, yscale = 1.0)
927
928 alignment.set_padding(padding_top = 8, padding_bottom = 8,
929 padding_left = 16, padding_right = 16)
930
931 logScroller = gtk.ScrolledWindow()
932 # FIXME: these should be constants in common
933 logScroller.set_policy(gtk.PolicyType.AUTOMATIC if pygobject
934 else gtk.POLICY_AUTOMATIC,
935 gtk.PolicyType.AUTOMATIC if pygobject
936 else gtk.POLICY_AUTOMATIC)
937 logScroller.set_shadow_type(gtk.ShadowType.IN if pygobject
938 else gtk.SHADOW_IN)
939 logView = gtk.TextView()
940 logView.set_editable(False)
941 logView.set_cursor_visible(False)
942 logScroller.add(logView)
943
944 logBox = gtk.VBox()
945 logBox.pack_start(logScroller, True, True, 0)
946 logBox.set_size_request(-1, 200)
947
948 alignment.add(logBox)
949
950 return (alignment, logView)
951
952 def _writeLog(self, msg, logView, isFault = False):
953 """Write the given message to the log."""
954 buffer = logView.get_buffer()
955 appendTextBuffer(buffer, msg, isFault = isFault)
956 logView.scroll_mark_onscreen(buffer.get_insert())
957
958 def _quit(self, what = None, force = False):
959 """Quit from the application."""
960 if force:
961 result=RESPONSETYPE_YES
962 else:
963 dialog = gtk.MessageDialog(parent = self._mainWindow,
964 type = MESSAGETYPE_QUESTION,
965 message_format = xstr("quit_question"))
966
967 dialog.add_button(xstr("button_no"), RESPONSETYPE_NO)
968 dialog.add_button(xstr("button_yes"), RESPONSETYPE_YES)
969
970 dialog.set_title(WINDOW_TITLE_BASE)
971 result = dialog.run()
972 dialog.hide()
973
974 if result==RESPONSETYPE_YES:
975 self._statusIcon.destroy()
976 return gtk.main_quit()
977
978 def _notebookPageSwitch(self, notebook, page, page_num):
979 """Called when the current page of the notebook has changed."""
980 if page_num==0:
981 gobject.idle_add(self._wizard.grabDefault)
982 else:
983 self._mainWindow.set_default(None)
984
985 def _editChecklist(self, menuItem):
986 """Callback for editing the checklists."""
987 self._checklistEditor.run()
988
989 def _editPreferences(self, menuItem):
990 """Callback for editing the preferences."""
991 self._clearHotkeys()
992 self._preferences.run(self.config)
993 self._setupTimeSync()
994 self._listenHotkeys()
995
996 def _setupTimeSync(self):
997 """Enable or disable the simulator time synchronization based on the
998 configuration."""
999 simulator = self._simulator
1000 if simulator is not None:
1001 if self.config.syncFSTime:
1002 simulator.enableTimeSync()
1003 else:
1004 simulator.disableTimeSync()
1005
1006 def _loadPIREP(self, menuItem):
1007 """Load a PIREP for sending."""
1008 dialog = self._getLoadPirepDialog()
1009
1010 if self._lastLoadedPIREP:
1011 dialog.set_current_folder(os.path.dirname(self._lastLoadedPIREP))
1012 else:
1013 pirepDirectory = self.config.pirepDirectory
1014 if pirepDirectory is not None:
1015 dialog.set_current_folder(pirepDirectory)
1016
1017 result = dialog.run()
1018 dialog.hide()
1019
1020 if result==RESPONSETYPE_OK:
1021 self._lastLoadedPIREP = text2unicode(dialog.get_filename())
1022
1023 pirep = PIREP.load(self._lastLoadedPIREP)
1024 if pirep is None:
1025 dialog = gtk.MessageDialog(parent = self._mainWindow,
1026 type = MESSAGETYPE_ERROR,
1027 message_format = xstr("loadPIREP_failed"))
1028 dialog.add_button(xstr("button_ok"), RESPONSETYPE_OK)
1029 dialog.set_title(WINDOW_TITLE_BASE)
1030 dialog.format_secondary_markup(xstr("loadPIREP_failed_sec"))
1031 dialog.run()
1032 dialog.hide()
1033 else:
1034 dialog = self._getSendLoadedDialog(pirep)
1035 dialog.show_all()
1036 while True:
1037 result = dialog.run()
1038
1039 if result==RESPONSETYPE_OK:
1040 self.sendPIREP(pirep)
1041 elif result==1:
1042 self._pirepViewer.setPIREP(pirep)
1043 self._pirepViewer.show_all()
1044 self._pirepViewer.run()
1045 self._pirepViewer.hide()
1046 else:
1047 break
1048
1049 dialog.hide()
1050
1051 def _getLoadPirepDialog(self):
1052 """Get the PIREP loading file chooser dialog.
1053
1054 If it is not created yet, it will be created."""
1055 if self._loadPIREPDialog is None:
1056 dialog = gtk.FileChooserDialog(title = WINDOW_TITLE_BASE + " - " +
1057 xstr("loadPIREP_browser_title"),
1058 action = FILE_CHOOSER_ACTION_OPEN,
1059 buttons = (gtk.STOCK_CANCEL,
1060 RESPONSETYPE_CANCEL,
1061 gtk.STOCK_OK, RESPONSETYPE_OK),
1062 parent = self._mainWindow)
1063 dialog.set_modal(True)
1064
1065
1066 filter = gtk.FileFilter()
1067 filter.set_name(xstr("file_filter_pireps"))
1068 filter.add_pattern("*.pirep")
1069 dialog.add_filter(filter)
1070
1071 filter = gtk.FileFilter()
1072 filter.set_name(xstr("file_filter_all"))
1073 filter.add_pattern("*.*")
1074 dialog.add_filter(filter)
1075
1076 self._loadPIREPDialog = dialog
1077
1078 return self._loadPIREPDialog
1079
1080 def _getSendLoadedDialog(self, pirep):
1081 """Get a dialog displaying the main information of the flight from the
1082 PIREP and providing Cancel and Send buttons."""
1083 dialog = gtk.Dialog(title = WINDOW_TITLE_BASE + " - " +
1084 xstr("loadPIREP_send_title"),
1085 parent = self._mainWindow,
1086 flags = DIALOG_MODAL)
1087
1088 contentArea = dialog.get_content_area()
1089
1090 label = gtk.Label(xstr("loadPIREP_send_help"))
1091 alignment = gtk.Alignment(xalign = 0.5, yalign = 0.5,
1092 xscale = 0.0, yscale = 0.0)
1093 alignment.set_padding(padding_top = 16, padding_bottom = 0,
1094 padding_left = 48, padding_right = 48)
1095 alignment.add(label)
1096 contentArea.pack_start(alignment, False, False, 8)
1097
1098 table = gtk.Table(5, 2)
1099 tableAlignment = gtk.Alignment(xalign = 0.5, yalign = 0.5,
1100 xscale = 0.0, yscale = 0.0)
1101 tableAlignment.set_padding(padding_top = 0, padding_bottom = 32,
1102 padding_left = 48, padding_right = 48)
1103 table.set_row_spacings(4)
1104 table.set_col_spacings(16)
1105 tableAlignment.add(table)
1106 contentArea.pack_start(tableAlignment, True, True, 8)
1107
1108 bookedFlight = pirep.bookedFlight
1109
1110 label = gtk.Label("<b>" + xstr("loadPIREP_send_flightno") + "</b>")
1111 label.set_use_markup(True)
1112 labelAlignment = gtk.Alignment(xalign = 1.0, yalign = 0.5,
1113 xscale = 0.0, yscale = 0.0)
1114 labelAlignment.add(label)
1115 table.attach(labelAlignment, 0, 1, 0, 1)
1116
1117 label = gtk.Label(bookedFlight.callsign)
1118 labelAlignment = gtk.Alignment(xalign = 0.0, yalign = 0.5,
1119 xscale = 0.0, yscale = 0.0)
1120 labelAlignment.add(label)
1121 table.attach(labelAlignment, 1, 2, 0, 1)
1122
1123 label = gtk.Label("<b>" + xstr("loadPIREP_send_date") + "</b>")
1124 label.set_use_markup(True)
1125 labelAlignment = gtk.Alignment(xalign = 1.0, yalign = 0.5,
1126 xscale = 0.0, yscale = 0.0)
1127 labelAlignment.add(label)
1128 table.attach(labelAlignment, 0, 1, 1, 2)
1129
1130 label = gtk.Label(str(bookedFlight.departureTime.date()))
1131 labelAlignment = gtk.Alignment(xalign = 0.0, yalign = 0.5,
1132 xscale = 0.0, yscale = 0.0)
1133 labelAlignment.add(label)
1134 table.attach(labelAlignment, 1, 2, 1, 2)
1135
1136 label = gtk.Label("<b>" + xstr("loadPIREP_send_from") + "</b>")
1137 label.set_use_markup(True)
1138 labelAlignment = gtk.Alignment(xalign = 1.0, yalign = 0.5,
1139 xscale = 0.0, yscale = 0.0)
1140 labelAlignment.add(label)
1141 table.attach(labelAlignment, 0, 1, 2, 3)
1142
1143 label = gtk.Label(bookedFlight.departureICAO)
1144 labelAlignment = gtk.Alignment(xalign = 0.0, yalign = 0.5,
1145 xscale = 0.0, yscale = 0.0)
1146 labelAlignment.add(label)
1147 table.attach(labelAlignment, 1, 2, 2, 3)
1148
1149 label = gtk.Label("<b>" + xstr("loadPIREP_send_to") + "</b>")
1150 label.set_use_markup(True)
1151 labelAlignment = gtk.Alignment(xalign = 1.0, yalign = 0.5,
1152 xscale = 0.0, yscale = 0.0)
1153 labelAlignment.add(label)
1154 table.attach(labelAlignment, 0, 1, 3, 4)
1155
1156 label = gtk.Label(bookedFlight.arrivalICAO)
1157 labelAlignment = gtk.Alignment(xalign = 0.0, yalign = 0.5,
1158 xscale = 0.0, yscale = 0.0)
1159 labelAlignment.add(label)
1160 table.attach(labelAlignment, 1, 2, 3, 4)
1161
1162 label = gtk.Label("<b>" + xstr("loadPIREP_send_rating") + "</b>")
1163 label.set_use_markup(True)
1164 labelAlignment = gtk.Alignment(xalign = 1.0, yalign = 0.5,
1165 xscale = 0.0, yscale = 0.0)
1166 labelAlignment.add(label)
1167 table.attach(labelAlignment, 0, 1, 4, 5)
1168
1169 rating = pirep.rating
1170 label = gtk.Label()
1171 if rating<0:
1172 label.set_markup('<b><span foreground="red">NO GO</span></b>')
1173 else:
1174 label.set_text("%.1f %%" % (rating,))
1175
1176 labelAlignment = gtk.Alignment(xalign = 0.0, yalign = 0.5,
1177 xscale = 0.0, yscale = 0.0)
1178 labelAlignment.add(label)
1179 table.attach(labelAlignment, 1, 2, 4, 5)
1180
1181 dialog.add_button(xstr("button_cancel"), RESPONSETYPE_REJECT)
1182 dialog.add_button(xstr("viewPIREP"), 1)
1183 dialog.add_button(xstr("sendPIREP"), RESPONSETYPE_OK)
1184
1185 return dialog
1186
1187 def sendPIREP(self, pirep, callback = None):
1188 """Send the given PIREP."""
1189 self.beginBusy(xstr("sendPIREP_busy"))
1190 self._sendPIREPCallback = callback
1191 self.webHandler.sendPIREP(self._pirepSentCallback, pirep)
1192
1193 def _pirepSentCallback(self, returned, result):
1194 """Callback for the PIREP sending result."""
1195 gobject.idle_add(self._handlePIREPSent, returned, result)
1196
1197 def _handlePIREPSent(self, returned, result):
1198 """Callback for the PIREP sending result."""
1199 self.endBusy()
1200 secondaryMarkup = None
1201 type = MESSAGETYPE_ERROR
1202 if returned:
1203 if result.success:
1204 type = MESSAGETYPE_INFO
1205 messageFormat = xstr("sendPIREP_success")
1206 secondaryMarkup = xstr("sendPIREP_success_sec")
1207 elif result.alreadyFlown:
1208 messageFormat = xstr("sendPIREP_already")
1209 secondaryMarkup = xstr("sendPIREP_already_sec")
1210 elif result.notAvailable:
1211 messageFormat = xstr("sendPIREP_notavail")
1212 else:
1213 messageFormat = xstr("sendPIREP_unknown")
1214 secondaryMarkup = xstr("sendPIREP_unknown_sec")
1215 else:
1216 print "PIREP sending failed", result
1217 messageFormat = xstr("sendPIREP_failed")
1218 secondaryMarkup = xstr("sendPIREP_failed_sec")
1219
1220 dialog = gtk.MessageDialog(parent = self._wizard.gui.mainWindow,
1221 type = type, message_format = messageFormat)
1222 dialog.add_button(xstr("button_ok"), RESPONSETYPE_OK)
1223 dialog.set_title(WINDOW_TITLE_BASE)
1224 if secondaryMarkup is not None:
1225 dialog.format_secondary_markup(secondaryMarkup)
1226
1227 dialog.run()
1228 dialog.hide()
1229
1230 callback = self._sendPIREPCallback
1231 self._sendPIREPCallback = None
1232 if callback is not None:
1233 callback(returned, result)
1234
1235 def _listenHotkeys(self):
1236 """Setup the hotkeys based on the configuration."""
1237 if self._hotkeySetID is None and self._simulator is not None:
1238 self._pilotHotkeyIndex = None
1239 self._checklistHotkeyIndex = None
1240
1241 hotkeys = []
1242
1243 config = self.config
1244 if config.enableSounds and config.pilotControlsSounds:
1245 self._pilotHotkeyIndex = len(hotkeys)
1246 hotkeys.append(config.pilotHotkey)
1247
1248 if config.enableChecklists:
1249 self._checklistHotkeyIndex = len(hotkeys)
1250 hotkeys.append(config.checklistHotkey)
1251
1252 if hotkeys:
1253 self._hotkeySetID = \
1254 self._simulator.listenHotkeys(hotkeys, self._handleHotkeys)
1255
1256 def _clearHotkeys(self):
1257 """Clear the hotkeys."""
1258 if self._hotkeySetID is not None:
1259 self._hotkeySetID=None
1260 self._simulator.clearHotkeys()
1261
1262 def _handleHotkeys(self, id, hotkeys):
1263 """Handle the hotkeys."""
1264 if id==self._hotkeySetID:
1265 for index in hotkeys:
1266 if index==self._pilotHotkeyIndex:
1267 print "gui.GUI._handleHotkeys: pilot hotkey pressed"
1268 self._flight.pilotHotkeyPressed()
1269 elif index==self._checklistHotkeyIndex:
1270 print "gui.GUI._handleHotkeys: checklist hotkey pressed"
1271 self._flight.checklistHotkeyPressed()
1272 else:
1273 print "gui.GUI._handleHotkeys: unhandled hotkey index:", index
1274
1275 def _showManual(self, menuitem):
1276 """Show the user's manual."""
1277 webbrowser.open(url ="file://" +
1278 os.path.join(self._programDirectory, "doc", "manual",
1279 getLanguage(), "index.html"),
1280 new = 1)
1281
1282 def _showAbout(self, menuitem):
1283 """Show the about dialog."""
1284 dialog = self._getAboutDialog()
1285 dialog.show_all()
1286 dialog.run()
1287 dialog.hide()
1288
1289 def _getAboutDialog(self):
1290 """Get the about dialog.
1291
1292 If it does not exist yet, it will be created."""
1293 if self._aboutDialog is None:
1294 self._aboutDialog = dialog = gtk.AboutDialog()
1295 dialog.set_transient_for(self._mainWindow)
1296 dialog.set_modal(True)
1297
1298 logoPath = os.path.join(self._programDirectory, "logo.png")
1299 logo = pixbuf_new_from_file(logoPath)
1300 dialog.set_logo(logo)
1301
1302 dialog.set_program_name(PROGRAM_NAME)
1303 dialog.set_version(const.VERSION)
1304 dialog.set_copyright("(c) 2012 by István Váradi")
1305 dialog.set_website("http://mlx.varadiistvan.hu")
1306 dialog.set_website_label(xstr("about_website"))
1307
1308 isHungarian = getLanguage()=="hu"
1309 authors = []
1310 for (familyName, firstName, role) in GUI._authors:
1311 authors.append("%s %s (%s)" % \
1312 (familyName if isHungarian else firstName,
1313 firstName if isHungarian else familyName,
1314 xstr("about_role_" + role)))
1315 dialog.set_authors(authors)
1316
1317 dialog.set_license(xstr("about_license"))
1318
1319 if not pygobject:
1320 gtk.about_dialog_set_url_hook(self._showAboutURL, None)
1321
1322 return self._aboutDialog
1323
1324 def _showAboutURL(self, dialog, link, user_data):
1325 """Show the about URL."""
1326 webbrowser.open(url = link, new = 1)
Note: See TracBrowser for help on using the repository browser.