source: src/mlx/gui/gui.py@ 105:d1c3dd71da77

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

The dialogs now have a proper parent window and title

File size: 23.7 KB
Line 
1# The main file for the GUI
2
3from statusicon import StatusIcon
4from statusbar import Statusbar
5from info import FlightInfo
6from update import Updater
7from mlx.gui.common import *
8from mlx.gui.flight import Wizard
9from mlx.gui.monitor import MonitorWindow
10
11import mlx.const as const
12import mlx.fs as fs
13import mlx.flight as flight
14import mlx.logger as logger
15import mlx.acft as acft
16import mlx.web as web
17
18import time
19import threading
20import sys
21
22#------------------------------------------------------------------------------
23
24class GUI(fs.ConnectionListener):
25 """The main GUI class."""
26 @staticmethod
27 def _formatFlightLogLine(timeStr, line):
28 """Format the given line for flight logging."""
29 if timeStr is not None:
30 line = timeStr + ": " + line
31 return line + "\n"
32
33 def __init__(self, programDirectory, config):
34 """Construct the GUI."""
35 gobject.threads_init()
36
37 self._programDirectory = programDirectory
38 self.config = config
39 self._connecting = False
40 self._reconnecting = False
41 self._connected = False
42 self._logger = logger.Logger(self)
43 self._flight = None
44 self._simulator = None
45 self._monitoring = False
46
47 self._stdioLock = threading.Lock()
48 self._stdioText = ""
49
50 self.webHandler = web.Handler()
51 self.webHandler.start()
52
53 self.toRestart = False
54
55 def build(self, iconDirectory):
56 """Build the GUI."""
57
58 window = gtk.Window()
59 window.set_title(WINDOW_TITLE_BASE)
60 window.set_icon_from_file(os.path.join(iconDirectory, "logo.ico"))
61 window.connect("delete-event",
62 lambda a, b: self.hideMainWindow())
63 window.connect("window-state-event", self._handleMainWindowState)
64 accelGroup = gtk.AccelGroup()
65 window.add_accel_group(accelGroup)
66
67 mainVBox = gtk.VBox()
68 window.add(mainVBox)
69
70 menuBar = self._buildMenuBar(accelGroup)
71 mainVBox.pack_start(menuBar, False, False, 0)
72
73 self._notebook = gtk.Notebook()
74 mainVBox.pack_start(self._notebook, True, True, 4)
75
76 self._wizard = Wizard(self)
77 label = gtk.Label("Fligh_t")
78 label.set_use_underline(True)
79 label.set_tooltip_text("Flight wizard")
80 self._notebook.append_page(self._wizard, label)
81
82 self._flightInfo = FlightInfo(self)
83 label = gtk.Label("Flight _info")
84 label.set_use_underline(True)
85 label.set_tooltip_text("Flight information")
86 self._notebook.append_page(self._flightInfo, label)
87 self._flightInfo.disable()
88
89 (logWidget, self._logView) = self._buildLogWidget()
90 label = gtk.Label("_Log")
91 label.set_use_underline(True)
92 label.set_tooltip_text("The log of your flight that will be sent to the MAVA website")
93 self._notebook.append_page(logWidget, label)
94
95 (self._debugLogWidget, self._debugLogView) = self._buildLogWidget()
96 self._debugLogWidget.show_all()
97
98 mainVBox.pack_start(gtk.HSeparator(), False, False, 0)
99
100 self._statusbar = Statusbar()
101 mainVBox.pack_start(self._statusbar, False, False, 0)
102
103 self._notebook.connect("switch-page", self._notebookPageSwitch)
104
105 self._monitorWindow = MonitorWindow(self, iconDirectory)
106 self._monitorWindow.add_accel_group(accelGroup)
107 self._monitorWindowX = None
108 self._monitorWindowY = None
109 self._selfToggling = False
110
111 window.show_all()
112 self._wizard.grabDefault()
113
114 self._mainWindow = window
115
116 self._statusIcon = StatusIcon(iconDirectory, self)
117
118 self._busyCursor = gdk.Cursor(gdk.CursorType.WATCH if pygobject
119 else gdk.WATCH)
120
121 @property
122 def mainWindow(self):
123 """Get the main window of the GUI."""
124 return self._mainWindow
125
126 @property
127 def logger(self):
128 """Get the logger used by us."""
129 return self._logger
130
131 @property
132 def simulator(self):
133 """Get the simulator used by us."""
134 return self._simulator
135
136 @property
137 def flight(self):
138 """Get the flight being performed."""
139 return self._flight
140
141 @property
142 def bookedFlight(self):
143 """Get the booked flight selected, if any."""
144 return self._wizard.bookedFlight
145
146 @property
147 def cargoWeight(self):
148 """Get the cargo weight."""
149 return self._wizard.cargoWeight
150
151 @property
152 def zfw(self):
153 """Get Zero-Fuel Weight calculated for the current flight."""
154 return self._wizard.zfw
155
156 @property
157 def filedCruiseAltitude(self):
158 """Get cruise altitude filed for the current flight."""
159 return self._wizard.filedCruiseAltitude
160
161 @property
162 def cruiseAltitude(self):
163 """Get cruise altitude set for the current flight."""
164 return self._wizard.cruiseAltitude
165
166 @property
167 def route(self):
168 """Get the flight route."""
169 return self._wizard.route
170
171 @property
172 def departureMETAR(self):
173 """Get the METAR of the deprature airport."""
174 return self._wizard.departureMETAR
175
176 @property
177 def arrivalMETAR(self):
178 """Get the METAR of the deprature airport."""
179 return self._wizard.arrivalMETAR
180
181 @property
182 def departureRunway(self):
183 """Get the name of the departure runway."""
184 return self._wizard.departureRunway
185
186 @property
187 def sid(self):
188 """Get the SID."""
189 return self._wizard.sid
190
191 @property
192 def v1(self):
193 """Get the V1 speed calculated for the flight."""
194 return self._wizard.v1
195
196 @property
197 def vr(self):
198 """Get the Vr speed calculated for the flight."""
199 return self._wizard.vr
200
201 @property
202 def v2(self):
203 """Get the V2 speed calculated for the flight."""
204 return self._wizard.v2
205
206 @property
207 def arrivalRunway(self):
208 """Get the arrival runway."""
209 return self._wizard.arrivalRunway
210
211 @property
212 def star(self):
213 """Get the STAR."""
214 return self._wizard.star
215
216 @property
217 def transition(self):
218 """Get the transition."""
219 return self._wizard.transition
220
221 @property
222 def approachType(self):
223 """Get the approach type."""
224 return self._wizard.approachType
225
226 @property
227 def vref(self):
228 """Get the Vref speed calculated for the flight."""
229 return self._wizard.vref
230
231 @property
232 def flightType(self):
233 """Get the flight type."""
234 return self._wizard.flightType
235
236 @property
237 def online(self):
238 """Get whether the flight was online or not."""
239 return self._wizard.online
240
241 @property
242 def comments(self):
243 """Get the comments."""
244 return self._flightInfo.comments
245
246 @property
247 def flightDefects(self):
248 """Get the flight defects."""
249 return self._flightInfo.flightDefects
250
251 @property
252 def delayCodes(self):
253 """Get the delay codes."""
254 return self._flightInfo.delayCodes
255
256 def run(self):
257 """Run the GUI."""
258 if self.config.autoUpdate:
259 self._updater = Updater(self,
260 self._programDirectory,
261 self.config.updateURL,
262 self._mainWindow)
263 self._updater.start()
264
265 gtk.main()
266
267 self._disconnect()
268
269 def connected(self, fsType, descriptor):
270 """Called when we have connected to the simulator."""
271 self._connected = True
272 self._logger.untimedMessage("Connected to the simulator %s" % (descriptor,))
273 gobject.idle_add(self._handleConnected, fsType, descriptor)
274
275 def _handleConnected(self, fsType, descriptor):
276 """Called when the connection to the simulator has succeeded."""
277 self._statusbar.updateConnection(self._connecting, self._connected)
278 self.endBusy()
279 if not self._reconnecting:
280 self._wizard.connected(fsType, descriptor)
281 self._reconnecting = False
282
283 def connectionFailed(self):
284 """Called when the connection failed."""
285 self._logger.untimedMessage("Connection to the simulator failed")
286 gobject.idle_add(self._connectionFailed)
287
288 def _connectionFailed(self):
289 """Called when the connection failed."""
290 self.endBusy()
291 self._statusbar.updateConnection(self._connecting, self._connected)
292
293 dialog = gtk.MessageDialog(parent = self._mainWindow,
294 type = MESSAGETYPE_ERROR,
295 message_format =
296 "Cannot connect to the simulator.")
297 dialog.set_title(WINDOW_TITLE_BASE)
298 dialog.format_secondary_markup("Rectify the situation, and press <b>Try again</b> "
299 "to try the connection again, "
300 "or <b>Cancel</b> to cancel the flight.")
301
302 dialog.add_button("_Cancel", 0)
303 dialog.add_button("_Try again", 1)
304 dialog.set_default_response(1)
305
306 result = dialog.run()
307 dialog.hide()
308 if result == 1:
309 self.beginBusy("Connecting to the simulator.")
310 self._simulator.reconnect()
311 else:
312 self.reset()
313
314 def disconnected(self):
315 """Called when we have disconnected from the simulator."""
316 self._connected = False
317 self._logger.untimedMessage("Disconnected from the simulator")
318
319 gobject.idle_add(self._disconnected)
320
321 def _disconnected(self):
322 """Called when we have disconnected from the simulator unexpectedly."""
323 self._statusbar.updateConnection(self._connecting, self._connected)
324
325 dialog = gtk.MessageDialog(type = MESSAGETYPE_ERROR,
326 message_format =
327 "The connection to the simulator failed unexpectedly.",
328 parent = self._mainWindow)
329 dialog.set_title(WINDOW_TITLE_BASE)
330 dialog.format_secondary_markup("If the simulator has crashed, restart it "
331 "and restore your flight as much as possible "
332 "to the state it was in before the crash. "
333 "Then press <b>Reconnect</b> to reconnect.\n\n"
334 "If you want to cancel the flight, press <b>Cancel</b>.")
335
336 dialog.add_button("_Cancel", 0)
337 dialog.add_button("_Reconnect", 1)
338 dialog.set_default_response(1)
339
340 result = dialog.run()
341 dialog.hide()
342 if result == 1:
343 self.beginBusy("Connecting to the simulator.")
344 self._reconnecting = True
345 self._simulator.reconnect()
346 else:
347 self.reset()
348
349 def enableFlightInfo(self):
350 """Enable the flight info tab."""
351 self._flightInfo.enable()
352
353 def reset(self):
354 """Reset the GUI."""
355 self._disconnect()
356
357 self._flightInfo.reset()
358 self._flightInfo.disable()
359 self.resetFlightStatus()
360
361 self._wizard.reset()
362 self._notebook.set_current_page(0)
363
364 self._logView.get_buffer().set_text("")
365
366 def _disconnect(self):
367 """Disconnect from the simulator if connected."""
368 self.stopMonitoring()
369
370 if self._connected:
371 self._flight.simulator.disconnect()
372 self._connected = False
373
374 self._connecting = False
375 self._reconnecting = False
376 self._statusbar.updateConnection(False, False)
377
378 def addFlightLogLine(self, timeStr, line):
379 """Write the given message line to the log."""
380 gobject.idle_add(self._writeLog,
381 GUI._formatFlightLogLine(timeStr, line),
382 self._logView)
383
384 def updateFlightLogLine(self, index, timeStr, line):
385 """Update the line with the given index."""
386 gobject.idle_add(self._updateFlightLogLine, index,
387 GUI._formatFlightLogLine(timeStr, line))
388
389 def _updateFlightLogLine(self, index, line):
390 """Replace the contents of the given line in the log."""
391 buffer = self._logView.get_buffer()
392 startIter = buffer.get_iter_at_line(index)
393 endIter = buffer.get_iter_at_line(index + 1)
394 buffer.delete(startIter, endIter)
395 buffer.insert(startIter, line)
396 self._logView.scroll_mark_onscreen(buffer.get_insert())
397
398 def check(self, flight, aircraft, logger, oldState, state):
399 """Update the data."""
400 gobject.idle_add(self._monitorWindow.setData, state)
401 gobject.idle_add(self._statusbar.updateTime, state.timestamp)
402
403 def resetFlightStatus(self):
404 """Reset the status of the flight."""
405 self._statusbar.resetFlightStatus()
406 self._statusbar.updateTime()
407 self._statusIcon.resetFlightStatus()
408
409 def setStage(self, stage):
410 """Set the stage of the flight."""
411 gobject.idle_add(self._setStage, stage)
412
413 def _setStage(self, stage):
414 """Set the stage of the flight."""
415 self._statusbar.setStage(stage)
416 self._statusIcon.setStage(stage)
417 self._wizard.setStage(stage)
418 if stage==const.STAGE_END:
419 self._disconnect()
420
421 def setRating(self, rating):
422 """Set the rating of the flight."""
423 gobject.idle_add(self._setRating, rating)
424
425 def _setRating(self, rating):
426 """Set the rating of the flight."""
427 self._statusbar.setRating(rating)
428 self._statusIcon.setRating(rating)
429
430 def setNoGo(self, reason):
431 """Set the rating of the flight to No-Go with the given reason."""
432 gobject.idle_add(self._setNoGo, reason)
433
434 def _setNoGo(self, reason):
435 """Set the rating of the flight."""
436 self._statusbar.setNoGo(reason)
437 self._statusIcon.setNoGo(reason)
438
439 def _handleMainWindowState(self, window, event):
440 """Hande a change in the state of the window"""
441 iconified = gdk.WindowState.ICONIFIED if pygobject \
442 else gdk.WINDOW_STATE_ICONIFIED
443 if (event.changed_mask&iconified)!=0 and (event.new_window_state&iconified)!=0:
444 self.hideMainWindow(savePosition = False)
445
446 def hideMainWindow(self, savePosition = True):
447 """Hide the main window and save its position."""
448 if savePosition:
449 (self._mainWindowX, self._mainWindowY) = \
450 self._mainWindow.get_window().get_root_origin()
451 else:
452 self._mainWindowX = self._mainWindowY = None
453 self._mainWindow.hide()
454 self._statusIcon.mainWindowHidden()
455 return True
456
457 def showMainWindow(self):
458 """Show the main window at its former position."""
459 if self._mainWindowX is not None and self._mainWindowY is not None:
460 self._mainWindow.move(self._mainWindowX, self._mainWindowY)
461
462 self._mainWindow.show()
463 self._mainWindow.deiconify()
464
465 self._statusIcon.mainWindowShown()
466
467 def toggleMainWindow(self):
468 """Toggle the main window."""
469 if self._mainWindow.get_visible():
470 self.hideMainWindow()
471 else:
472 self.showMainWindow()
473
474 def hideMonitorWindow(self, savePosition = True):
475 """Hide the monitor window."""
476 if savePosition:
477 (self._monitorWindowX, self._monitorWindowY) = \
478 self._monitorWindow.get_window().get_root_origin()
479 else:
480 self._monitorWindowX = self._monitorWindowY = None
481 self._monitorWindow.hide()
482 self._statusIcon.monitorWindowHidden()
483 if self._showMonitorMenuItem.get_active():
484 self._selfToggling = True
485 self._showMonitorMenuItem.set_active(False)
486 return True
487
488 def showMonitorWindow(self):
489 """Show the monitor window."""
490 if self._monitorWindowX is not None and self._monitorWindowY is not None:
491 self._monitorWindow.move(self._monitorWindowX, self._monitorWindowY)
492 self._monitorWindow.show_all()
493 self._statusIcon.monitorWindowShown()
494 if not self._showMonitorMenuItem.get_active():
495 self._selfToggling = True
496 self._showMonitorMenuItem.set_active(True)
497
498 def _toggleMonitorWindow(self, menuItem):
499 if self._selfToggling:
500 self._selfToggling = False
501 elif self._monitorWindow.get_visible():
502 self.hideMonitorWindow()
503 else:
504 self.showMonitorWindow()
505
506 def restart(self):
507 """Quit and restart the application."""
508 self.toRestart = True
509 self._quit(force = True)
510
511 def flushStdIO(self):
512 """Flush any text to the standard error that could not be logged."""
513 if self._stdioText:
514 sys.__stderr__.write(self._stdioText)
515
516 def writeStdIO(self, text):
517 """Write the given text into standard I/O log."""
518 with self._stdioLock:
519 self._stdioText += text
520
521 gobject.idle_add(self._writeStdIO)
522
523 def beginBusy(self, message):
524 """Begin a period of background processing."""
525 self._wizard.set_sensitive(False)
526 self._mainWindow.get_window().set_cursor(self._busyCursor)
527 self._statusbar.updateBusyState(message)
528
529 def endBusy(self):
530 """End a period of background processing."""
531 self._mainWindow.get_window().set_cursor(None)
532 self._wizard.set_sensitive(True)
533 self._statusbar.updateBusyState(None)
534
535 def _writeStdIO(self):
536 """Perform the real writing."""
537 with self._stdioLock:
538 text = self._stdioText
539 self._stdioText = ""
540 if not text: return
541
542 lines = text.splitlines()
543 if text[-1]=="\n":
544 text = ""
545 else:
546 text = lines[-1]
547 lines = lines[:-1]
548
549 for line in lines:
550 #print >> sys.__stdout__, line
551 self._writeLog(line + "\n", self._debugLogView)
552
553 if text:
554 #print >> sys.__stdout__, text,
555 self._writeLog(text, self._debugLogView)
556
557 def connectSimulator(self, aircraftType):
558 """Connect to the simulator for the first time."""
559 self._logger.reset()
560
561 self._flight = flight.Flight(self._logger, self)
562 self._flight.aircraftType = aircraftType
563 self._flight.aircraft = acft.Aircraft.create(self._flight)
564 self._flight.aircraft._checkers.append(self)
565
566 if self._simulator is None:
567 self._simulator = fs.createSimulator(const.SIM_MSFS9, self)
568
569 self._flight.simulator = self._simulator
570
571 self.beginBusy("Connecting to the simulator...")
572 self._statusbar.updateConnection(self._connecting, self._connected)
573
574 self._connecting = True
575 self._simulator.connect(self._flight.aircraft)
576
577 def startMonitoring(self):
578 """Start monitoring."""
579 if not self._monitoring:
580 self.simulator.startMonitoring()
581 self._monitoring = True
582
583 def stopMonitoring(self):
584 """Stop monitoring."""
585 if self._monitoring:
586 self.simulator.stopMonitoring()
587 self._monitoring = False
588
589 def _buildMenuBar(self, accelGroup):
590 """Build the main menu bar."""
591 menuBar = gtk.MenuBar()
592
593 fileMenuItem = gtk.MenuItem("File")
594 fileMenu = gtk.Menu()
595 fileMenuItem.set_submenu(fileMenu)
596 menuBar.append(fileMenuItem)
597
598 quitMenuItem = gtk.ImageMenuItem(gtk.STOCK_QUIT)
599 quitMenuItem.set_use_stock(True)
600 quitMenuItem.add_accelerator("activate", accelGroup,
601 ord("q"), CONTROL_MASK,
602 ACCEL_VISIBLE)
603 quitMenuItem.connect("activate", self._quit)
604 fileMenu.append(quitMenuItem)
605
606
607 viewMenuItem = gtk.MenuItem("View")
608 viewMenu = gtk.Menu()
609 viewMenuItem.set_submenu(viewMenu)
610 menuBar.append(viewMenuItem)
611
612 self._showMonitorMenuItem = gtk.CheckMenuItem()
613 self._showMonitorMenuItem.set_label("Show _monitor window")
614 self._showMonitorMenuItem.set_use_underline(True)
615 self._showMonitorMenuItem.set_active(False)
616 self._showMonitorMenuItem.add_accelerator("activate", accelGroup,
617 ord("m"), CONTROL_MASK,
618 ACCEL_VISIBLE)
619 self._showMonitorMenuItem.connect("toggled", self._toggleMonitorWindow)
620 viewMenu.append(self._showMonitorMenuItem)
621
622 showDebugMenuItem = gtk.CheckMenuItem()
623 showDebugMenuItem.set_label("Show _debug log")
624 showDebugMenuItem.set_use_underline(True)
625 showDebugMenuItem.set_active(False)
626 showDebugMenuItem.add_accelerator("activate", accelGroup,
627 ord("d"), CONTROL_MASK,
628 ACCEL_VISIBLE)
629 showDebugMenuItem.connect("toggled", self._toggleDebugLog)
630 viewMenu.append(showDebugMenuItem)
631
632 return menuBar
633
634 def _toggleDebugLog(self, menuItem):
635 """Toggle the debug log."""
636 if menuItem.get_active():
637 label = gtk.Label("_Debug log")
638 label.set_use_underline(True)
639 label.set_tooltip_text("Log with debugging information.")
640 self._debugLogPage = self._notebook.append_page(self._debugLogWidget, label)
641 self._notebook.set_current_page(self._debugLogPage)
642 else:
643 self._notebook.remove_page(self._debugLogPage)
644
645 def _buildLogWidget(self):
646 """Build the widget for the log."""
647 alignment = gtk.Alignment(xscale = 1.0, yscale = 1.0)
648
649 alignment.set_padding(padding_top = 8, padding_bottom = 8,
650 padding_left = 16, padding_right = 16)
651
652 logScroller = gtk.ScrolledWindow()
653 # FIXME: these should be constants in common
654 logScroller.set_policy(gtk.PolicyType.AUTOMATIC if pygobject
655 else gtk.POLICY_AUTOMATIC,
656 gtk.PolicyType.AUTOMATIC if pygobject
657 else gtk.POLICY_AUTOMATIC)
658 logScroller.set_shadow_type(gtk.ShadowType.IN if pygobject
659 else gtk.SHADOW_IN)
660 logView = gtk.TextView()
661 logView.set_editable(False)
662 logScroller.add(logView)
663
664 logBox = gtk.VBox()
665 logBox.pack_start(logScroller, True, True, 0)
666 logBox.set_size_request(-1, 200)
667
668 alignment.add(logBox)
669
670 return (alignment, logView)
671
672 def _writeLog(self, msg, logView):
673 """Write the given message to the log."""
674 buffer = logView.get_buffer()
675 buffer.insert(buffer.get_end_iter(), msg)
676 logView.scroll_mark_onscreen(buffer.get_insert())
677
678 def _quit(self, what = None, force = False):
679 """Quit from the application."""
680 if force:
681 result=RESPONSETYPE_YES
682 else:
683 dialog = gtk.MessageDialog(parent = self._mainWindow,
684 type = MESSAGETYPE_QUESTION,
685 buttons = BUTTONSTYPE_YES_NO,
686 message_format =
687 "Are you sure to quit the logger?")
688 dialog.set_title(WINDOW_TITLE_BASE)
689 result = dialog.run()
690 dialog.hide()
691
692 if result==RESPONSETYPE_YES:
693 self._statusIcon.destroy()
694 return gtk.main_quit()
695
696 def _notebookPageSwitch(self, notebook, page, page_num):
697 """Called when the current page of the notebook has changed."""
698 if page_num==0:
699 gobject.idle_add(self._wizard.grabDefault)
700 else:
701 self._mainWindow.set_default(None)
Note: See TracBrowser for help on using the repository browser.