source: src/mlx/gui/common.py@ 838:caac643bc702

Last change on this file since 838:caac643bc702 was 837:739af4588b42, checked in by István Váradi <ivaradi@…>, 8 years ago

Time data entry widget

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