source: src/mlx/gui/common.py@ 996:8035d80d5feb

python3
Last change on this file since 996:8035d80d5feb was 996:8035d80d5feb, checked in by István Váradi <ivaradi@…>, 5 years ago

Using 'Gtk' instead of 'gtk' (re #347)

File size: 19.8 KB
Line 
1
2from mlx.common import *
3
4import mlx.const as _const
5from mlx.i18n import xstr
6
7from mlx.util import secondaryInstallation
8
9import os
10import time
11import calendar
12
13#-----------------------------------------------------------------------------
14
15## @package mlx.gui.common
16#
17# Common definitions and utilities for the GUI
18
19#-----------------------------------------------------------------------------
20
21appIndicator = False
22
23import gi
24gi.require_version("Gdk", "3.0")
25from gi.repository import Gdk as gdk
26from gi.repository import GdkPixbuf as gdkPixbuf
27gi.require_version("Gtk", "3.0")
28from gi.repository import Gtk
29try:
30 gi.require_version("AppIndicator3", "0.1")
31 from gi.repository import AppIndicator3 as appindicator
32 appIndicator = True
33except:
34 pass
35from gi.repository import Pango as pango
36
37
38MESSAGETYPE_ERROR = Gtk.MessageType.ERROR
39MESSAGETYPE_QUESTION = Gtk.MessageType.QUESTION
40MESSAGETYPE_INFO = Gtk.MessageType.INFO
41RESPONSETYPE_NONE = Gtk.ResponseType.NONE
42RESPONSETYPE_OK = Gtk.ResponseType.OK
43RESPONSETYPE_YES = Gtk.ResponseType.YES
44RESPONSETYPE_NO = Gtk.ResponseType.NO
45RESPONSETYPE_ACCEPT = Gtk.ResponseType.ACCEPT
46RESPONSETYPE_REJECT = Gtk.ResponseType.REJECT
47RESPONSETYPE_CANCEL = Gtk.ResponseType.CANCEL
48ACCEL_VISIBLE = Gtk.AccelFlags.VISIBLE
49CONTROL_MASK = gdk.ModifierType.CONTROL_MASK
50DIALOG_MODAL = Gtk.DialogFlags.MODAL
51WRAP_WORD = Gtk.WrapMode.WORD
52JUSTIFY_CENTER = Gtk.Justification.CENTER
53JUSTIFY_LEFT = Gtk.Justification.LEFT
54
55CONTROL_MASK = gdk.ModifierType.CONTROL_MASK
56SHIFT_MASK = gdk.ModifierType.SHIFT_MASK
57BUTTON1_MASK = gdk.ModifierType.BUTTON1_MASK
58
59SCROLL_UP = gdk.ScrollDirection.UP
60SCROLL_DOWN = gdk.ScrollDirection.DOWN
61
62SPIN_USER_DEFINED = Gtk.SpinType.USER_DEFINED
63
64FILE_CHOOSER_ACTION_SELECT_FOLDER = Gtk.FileChooserAction.SELECT_FOLDER
65FILE_CHOOSER_ACTION_OPEN = Gtk.FileChooserAction.OPEN
66FILE_CHOOSER_ACTION_SAVE = Gtk.FileChooserAction.SAVE
67
68SELECTION_MULTIPLE = Gtk.SelectionMode.MULTIPLE
69
70SHADOW_IN = Gtk.ShadowType.IN
71SHADOW_NONE = Gtk.ShadowType.NONE
72
73POLICY_AUTOMATIC = Gtk.PolicyType.AUTOMATIC
74POLICY_NEVER = Gtk.PolicyType.NEVER
75POLICY_ALWAYS = Gtk.PolicyType.ALWAYS
76
77WEIGHT_NORMAL = pango.Weight.NORMAL
78WEIGHT_BOLD = pango.Weight.BOLD
79
80WINDOW_STATE_ICONIFIED = gdk.WindowState.ICONIFIED
81WINDOW_STATE_WITHDRAWN = gdk.WindowState.WITHDRAWN
82
83SORT_ASCENDING = Gtk.SortType.ASCENDING
84SORT_DESCENDING = Gtk.SortType.DESCENDING
85
86EVENT_BUTTON_PRESS = gdk.EventType.BUTTON_PRESS
87
88TREE_VIEW_COLUMN_FIXED = Gtk.TreeViewColumnSizing.FIXED
89
90FILL = Gtk.AttachOptions.FILL
91EXPAND = Gtk.AttachOptions.EXPAND
92
93UPDATE_IF_VALID = Gtk.SpinButtonUpdatePolicy.IF_VALID
94
95SELECTION_MULTIPLE = Gtk.SelectionMode.MULTIPLE
96
97pixbuf_new_from_file = gdkPixbuf.Pixbuf.new_from_file
98
99import codecs
100_utf8Decoder = codecs.getdecoder("utf-8")
101
102import cairo
103
104#------------------------------------------------------------------------------
105
106class FlightStatusHandler(object):
107 """Base class for objects that handle the flight status in some way."""
108 def __init__(self):
109 self._stage = None
110 self._rating = 100
111 self._noGoReason = None
112
113 def resetFlightStatus(self):
114 """Reset the flight status."""
115 self._stage = None
116 self._rating = 100
117 self._noGoReason = None
118 self._updateFlightStatus()
119
120 def setStage(self, stage):
121 """Set the stage of the flight."""
122 if stage!=self._stage:
123 self._stage = stage
124 self._updateFlightStatus()
125
126 def setRating(self, rating):
127 """Set the rating to the given value."""
128 if rating!=self._rating:
129 self._rating = rating
130 if self._noGoReason is None:
131 self._updateFlightStatus()
132
133 def setNoGo(self, reason):
134 """Set a No-Go condition with the given reason."""
135 if self._noGoReason is None:
136 self._noGoReason = reason
137 self._updateFlightStatus()
138
139#------------------------------------------------------------------------------
140
141class IntegerEntry(Gtk.Entry):
142 """An entry that allows only either an empty value, or an integer."""
143 def __init__(self, defaultValue = None):
144 """Construct the entry."""
145 Gtk.Entry.__init__(self)
146
147 self.set_alignment(1.0)
148
149 self._defaultValue = defaultValue
150 self._currentInteger = defaultValue
151 self._selfSetting = False
152 self._set_text()
153
154 self.connect("changed", self._handle_changed)
155
156 def get_int(self):
157 """Get the integer."""
158 return self._currentInteger
159
160 def reset(self):
161 """Reset the integer."""
162 self.set_int(None)
163
164 def set_int(self, value):
165 """Set the integer."""
166 if value!=self._currentInteger:
167 self._currentInteger = value
168 self.emit("integer-changed", self._currentInteger)
169 self._set_text()
170
171 def _handle_changed(self, widget):
172 """Handle the changed signal."""
173 if self._selfSetting:
174 return
175 text = self.get_text()
176 if text=="":
177 self.set_int(self._defaultValue)
178 else:
179 try:
180 self.set_int(int(text))
181 except:
182 self._set_text()
183
184 def _set_text(self):
185 """Set the text value from the current integer."""
186 self._selfSetting = True
187 self.set_text("" if self._currentInteger is None
188 else str(self._currentInteger))
189 self._selfSetting = False
190
191#------------------------------------------------------------------------------
192
193class TimeEntry(Gtk.Entry):
194 """Widget to display and edit a time value in HH:MM format."""
195 def __init__(self):
196 """Construct the entry"""
197 super(TimeEntry, self).__init__()
198 self.set_max_width_chars(5)
199
200 self.connect("insert-text", self._insertText)
201 self.connect("delete-text", self._deleteText)
202 self.connect("focus-out-event", self._focusOutEvent)
203
204 @property
205 def hour(self):
206 """Get the hour from the current text"""
207 text = self.get_text()
208 if not text or text==":":
209 return 0
210
211 words = text.split(":")
212 if len(words)==1:
213 return 0
214 elif len(words)>=2:
215 return 0 if len(words[0])==0 else int(words[0])
216 else:
217 return 0
218
219 @property
220 def minute(self):
221 """Get the hour from the current text"""
222 text = self.get_text()
223 if not text or text==":":
224 return 0
225
226 words = text.split(":")
227 if len(words)==1:
228 return 0 if len(words[0])==0 else int(words[0])
229 elif len(words)>=2:
230 return 0 if len(words[1])==0 else int(words[1])
231 else:
232 return 0
233
234 @property
235 def minutes(self):
236 """Get the time in minutes, i.e. hour*60+minute."""
237 return self.hour * 60 + self.minute
238
239 def setTimestamp(self, timestamp):
240 """Set the hour and minute from the given timestamp in UTC."""
241 tm = time.gmtime(timestamp)
242 self.set_text("%02d:%02d" % (tm.tm_hour, tm.tm_min))
243
244 def getTimestampFrom(self, timestamp):
245 """Get the timestamp by replacing the hour and minute from the given
246 timestamp with what is set in this widget."""
247 tm = time.gmtime(timestamp)
248 ts = calendar.timegm((tm.tm_year, tm.tm_mon, tm.tm_mday,
249 self.hour, self.minute, 0,
250 tm.tm_wday, tm.tm_yday, tm.tm_isdst))
251
252 if ts > (timestamp + (16*60*60)):
253 ts -= 24*60*60
254 elif (ts + 16*60*60) < timestamp:
255 ts += 24*60*60
256
257 return ts
258
259 def _focusOutEvent(self, widget, event):
260 """Reformat the text to match pattern HH:MM"""
261 text = "%02d:%02d" % (self.hour, self.minute)
262 if text!=self.get_text():
263 self.set_text(text)
264
265 def _insertText(self, entry, text, length, position):
266 """Called when some text is inserted into the entry."""
267 text=text[:length]
268 currentText = self.get_text()
269 position = self.get_position()
270 newText = currentText[:position] + text + currentText[position:]
271 self._checkText(newText, "insert-text")
272
273 def _deleteText(self, entry, start, end):
274 """Called when some text is erased from the entry."""
275 currentText = self.get_text()
276 newText = currentText[:start] + currentText[end:]
277 self._checkText(newText, "delete-text")
278
279 def _checkText(self, newText, signal):
280 """Check the given text.
281
282 If it is not suitable, stop the emission of the signal to prevent the
283 change from appearing."""
284 if not newText or newText==":":
285 return
286
287 words = newText.split(":")
288 if (len(words)==1 and
289 len(words[0])<=2 and (len(words[0])==0 or
290 (words[0].isdigit() and int(words[0])<60))) or \
291 (len(words)==2 and
292 len(words[0])<=2 and (len(words[0])==0 or
293 (words[0].isdigit() and int(words[0])<24)) and
294 len(words[1])<=2 and (len(words[1])==0 or
295 (words[1].isdigit() and int(words[1])<60))):
296 pass
297 else:
298 Gtk.gdk.display_get_default().beep()
299 self.stop_emission(signal)
300
301#------------------------------------------------------------------------------
302
303class CredentialsDialog(Gtk.Dialog):
304 """A dialog window to ask for a user name and a password."""
305 def __init__(self, gui, userName, password,
306 titleLabel, cancelButtonLabel, okButtonLabel,
307 userNameLabel, userNameTooltip,
308 passwordLabel, passwordTooltip,
309 infoText = None,
310 rememberPassword = None,
311 rememberLabel = None, rememberTooltip = None):
312 """Construct the dialog."""
313 super(CredentialsDialog, self).__init__(WINDOW_TITLE_BASE + " - " +
314 titleLabel,
315 gui.mainWindow,
316 DIALOG_MODAL)
317 self.add_button(cancelButtonLabel, RESPONSETYPE_CANCEL)
318 self.add_button(okButtonLabel, RESPONSETYPE_OK)
319
320 contentArea = self.get_content_area()
321
322 contentAlignment = Gtk.Alignment(xalign = 0.5, yalign = 0.5,
323 xscale = 0.0, yscale = 0.0)
324 contentAlignment.set_padding(padding_top = 4, padding_bottom = 16,
325 padding_left = 8, padding_right = 8)
326
327 contentArea.pack_start(contentAlignment, False, False, 0)
328
329 contentVBox = Gtk.VBox()
330 contentAlignment.add(contentVBox)
331
332 if infoText is not None:
333 label = Gtk.Label(infoText)
334 label.set_alignment(0.0, 0.0)
335
336 contentVBox.pack_start(label, False, False, 0)
337
338 tableAlignment = Gtk.Alignment(xalign = 0.5, yalign = 0.5,
339 xscale = 0.0, yscale = 0.0)
340 tableAlignment.set_padding(padding_top = 24, padding_bottom = 0,
341 padding_left = 0, padding_right = 0)
342
343 table = Gtk.Table(3, 2)
344 table.set_row_spacings(4)
345 table.set_col_spacings(16)
346 table.set_homogeneous(False)
347
348 tableAlignment.add(table)
349 contentVBox.pack_start(tableAlignment, True, True, 0)
350
351 label = Gtk.Label(userNameLabel)
352 label.set_use_underline(True)
353 label.set_alignment(0.0, 0.5)
354 table.attach(label, 0, 1, 0, 1)
355
356 self._userName = Gtk.Entry()
357 self._userName.set_width_chars(16)
358 # FIXME: enabled the OK button only when there is something in thr
359 # user name and password fields
360 #self._userName.connect("changed",
361 # lambda button: self._updateForwardButton())
362 self._userName.set_tooltip_text(userNameTooltip)
363 self._userName.set_text(userName)
364 table.attach(self._userName, 1, 2, 0, 1)
365 label.set_mnemonic_widget(self._userName)
366
367 label = Gtk.Label(passwordLabel)
368 label.set_use_underline(True)
369 label.set_alignment(0.0, 0.5)
370 table.attach(label, 0, 1, 1, 2)
371
372 self._password = Gtk.Entry()
373 self._password.set_visibility(False)
374 #self._password.connect("changed",
375 # lambda button: self._updateForwardButton())
376 self._password.set_tooltip_text(passwordTooltip)
377 self._password.set_text(password)
378 table.attach(self._password, 1, 2, 1, 2)
379 label.set_mnemonic_widget(self._password)
380
381 if rememberPassword is not None:
382 self._rememberButton = Gtk.CheckButton(rememberLabel)
383 self._rememberButton.set_use_underline(True)
384 self._rememberButton.set_tooltip_text(rememberTooltip)
385 self._rememberButton.set_active(rememberPassword)
386 table.attach(self._rememberButton, 1, 2, 2, 3, ypadding = 8)
387 else:
388 self._rememberButton = None
389
390 @property
391 def userName(self):
392 """Get the user name entered."""
393 return self._userName.get_text()
394
395 @property
396 def password(self):
397 """Get the password entered."""
398 return self._password.get_text()
399
400 @property
401 def rememberPassword(self):
402 """Get whether the password is to be remembered."""
403 return None if self._rememberButton is None \
404 else self._rememberButton.get_active()
405
406 def run(self):
407 """Run the dialog."""
408 self.show_all()
409
410 response = super(CredentialsDialog, self).run()
411
412 self.hide()
413
414 return response
415
416#------------------------------------------------------------------------------
417
418GObject.signal_new("integer-changed", IntegerEntry, GObject.SIGNAL_RUN_FIRST,
419 None, (object,))
420
421#------------------------------------------------------------------------------
422
423PROGRAM_NAME = "MAVA Logger X"
424
425WINDOW_TITLE_BASE = PROGRAM_NAME + " " + _const.VERSION
426if secondaryInstallation:
427 WINDOW_TITLE_BASE += " (" + xstr("secondary") + ")"
428
429#------------------------------------------------------------------------------
430
431# A mapping of aircraft types to their screen names
432aircraftNames = { _const.AIRCRAFT_B736 : xstr("aircraft_b736"),
433 _const.AIRCRAFT_B737 : xstr("aircraft_b737"),
434 _const.AIRCRAFT_B738 : xstr("aircraft_b738"),
435 _const.AIRCRAFT_B738C : xstr("aircraft_b738c"),
436 _const.AIRCRAFT_B732 : xstr("aircraft_b732"),
437 _const.AIRCRAFT_B733 : xstr("aircraft_b733"),
438 _const.AIRCRAFT_B734 : xstr("aircraft_b734"),
439 _const.AIRCRAFT_B735 : xstr("aircraft_b735"),
440 _const.AIRCRAFT_DH8D : xstr("aircraft_dh8d"),
441 _const.AIRCRAFT_B762 : xstr("aircraft_b762"),
442 _const.AIRCRAFT_B763 : xstr("aircraft_b763"),
443 _const.AIRCRAFT_CRJ2 : xstr("aircraft_crj2"),
444 _const.AIRCRAFT_F70 : xstr("aircraft_f70"),
445 _const.AIRCRAFT_DC3 : xstr("aircraft_dc3"),
446 _const.AIRCRAFT_T134 : xstr("aircraft_t134"),
447 _const.AIRCRAFT_T154 : xstr("aircraft_t154"),
448 _const.AIRCRAFT_YK40 : xstr("aircraft_yk40"),
449 _const.AIRCRAFT_B462 : xstr("aircraft_b462") }
450
451#------------------------------------------------------------------------------
452
453aircraftFamilyNames = {
454 _const.AIRCRAFT_FAMILY_B737NG: xstr("aircraft_family_b737ng"),
455
456 _const.AIRCRAFT_FAMILY_B737CL: xstr("aircraft_family_b737cl"),
457
458 _const.AIRCRAFT_FAMILY_DH8D: xstr("aircraft_family_dh8d"),
459
460 _const.AIRCRAFT_FAMILY_B767: xstr("aircraft_family_b767"),
461
462 _const.AIRCRAFT_FAMILY_CRJ2: xstr("aircraft_family_crj2"),
463
464 _const.AIRCRAFT_FAMILY_F70: xstr("aircraft_family_f70"),
465
466 _const.AIRCRAFT_FAMILY_DC3: xstr("aircraft_family_dc3"),
467
468 _const.AIRCRAFT_FAMILY_T134: xstr("aircraft_family_t134"),
469
470 _const.AIRCRAFT_FAMILY_T154: xstr("aircraft_family_t154"),
471
472 _const.AIRCRAFT_FAMILY_YK40: xstr("aircraft_family_yk40"),
473
474 _const.AIRCRAFT_FAMILY_B462: xstr("aircraft_family_b462")
475}
476
477#------------------------------------------------------------------------------
478
479def formatFlightLogLine(timeStr, line):
480 """Format the given flight log line."""
481 """Format the given line for flight logging."""
482 if timeStr is not None:
483 line = timeStr + ": " + line
484 return line + "\n"
485
486#------------------------------------------------------------------------------
487
488def addFaultTag(buffer):
489 """Add a tag named 'fault' to the given buffer."""
490 buffer.create_tag("fault", foreground="red", weight=WEIGHT_BOLD)
491
492#------------------------------------------------------------------------------
493
494def appendTextBuffer(buffer, text, isFault = False):
495 """Append the given line at the end of the given text buffer.
496
497 If isFault is set, use the tag named 'fault'."""
498 insertTextBuffer(buffer, buffer.get_end_iter(), text, isFault)
499
500#------------------------------------------------------------------------------
501
502def insertTextBuffer(buffer, iter, text, isFault = False):
503 """Insert the given line into the given text buffer at the given iterator.
504
505 If isFault is set, use the tag named 'fault' else use the tag named
506 'normal'."""
507 line = iter.get_line()
508
509 buffer.insert(iter, text)
510
511 iter0 = buffer.get_iter_at_line(line)
512 iter1 = buffer.get_iter_at_line(line+1)
513 if isFault:
514 buffer.apply_tag_by_name("fault", iter0, iter1)
515 else:
516 buffer.remove_all_tags(iter0, iter1)
517
518#------------------------------------------------------------------------------
519
520def askYesNo(question, parent = None, title = WINDOW_TITLE_BASE):
521 """Ask a Yes/No question.
522
523 Return a boolean indicating the answer."""
524 dialog = Gtk.MessageDialog(parent = parent,
525 type = MESSAGETYPE_QUESTION,
526 message_format = question)
527
528 dialog.add_button(xstr("button_no"), RESPONSETYPE_NO)
529 dialog.add_button(xstr("button_yes"), RESPONSETYPE_YES)
530
531 dialog.set_title(title)
532 result = dialog.run()
533 dialog.hide()
534
535 return result==RESPONSETYPE_YES
536
537#------------------------------------------------------------------------------
538
539def errorDialog(message, parent = None, secondary = None,
540 title = WINDOW_TITLE_BASE):
541 """Display an error dialog box with the given message."""
542 dialog = Gtk.MessageDialog(parent = parent,
543 type = MESSAGETYPE_ERROR,
544 message_format = message)
545 dialog.add_button(xstr("button_ok"), RESPONSETYPE_OK)
546 dialog.set_title(title)
547 if secondary is not None:
548 dialog.format_secondary_markup(secondary)
549
550 dialog.run()
551 dialog.hide()
552
553#------------------------------------------------------------------------------
554
555def communicationErrorDialog(parent = None, title = WINDOW_TITLE_BASE):
556 """Display a communication error dialog."""
557 errorDialog(xstr("error_communication"), parent = parent,
558 secondary = xstr("error_communication_secondary"),
559 title = title)
560
561#------------------------------------------------------------------------------
562
563def createFlightTypeComboBox():
564 flightTypeModel = Gtk.ListStore(str, int)
565 for type in _const.flightTypes:
566 name = "flighttype_" + _const.flightType2string(type)
567 flightTypeModel.append([xstr(name), type])
568
569 flightType = Gtk.ComboBox(model = flightTypeModel)
570 renderer = Gtk.CellRendererText()
571 flightType.pack_start(renderer, True)
572 flightType.add_attribute(renderer, "text", 0)
573
574 return flightType
575
576#------------------------------------------------------------------------------
577
578def getTextViewText(textView):
579 """Get the text from the given text view."""
580 buffer = textView.get_buffer()
581 return buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), True)
582
583#------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.