1
# This file is part of MAUS: http://micewww.pp.rl.ac.uk/projects/maus
3
# MAUS is free software: you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation, either version 3 of the License, or
6
# (at your option) any later version.
8
# MAUS is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with MAUS. If not, see <http://www.gnu.org/licenses/>.
16
# pylint does not import ROOT
17
# pylint: disable=E1101
20
Module handles GUI elements for making python GUIs within MAUS.
22
An interface is provided that enables layout to be defined using json
30
KNORMAL = ROOT.TGLayoutHints.kLHintsNormal
31
KEXPANDX = ROOT.TGLayoutHints.kLHintsExpandX
32
KCENTERX = ROOT.TGLayoutHints.kLHintsCenterX
33
DEFAULT_TEXT_LENGTH = 10
38
Select a layout option for the element
39
- option: string layout option - either normal (default), close (close
40
packed), close_v, close_h (close packed in only vertical or
42
Returns an instance of ROOT.TGLayoutHints.
44
Throws a ValueError if the option is not recognised.
48
if option == "normal":
49
return ROOT.TGLayoutHints(KNORMAL, normal, normal, normal, normal)
50
if option == "close": # close_packed
51
return ROOT.TGLayoutHints(KNORMAL, close, close, close, close)
52
if option == "close_v": # close packed vertically
53
return ROOT.TGLayoutHints(KNORMAL, normal, normal, close, close)
54
if option == "close_h": # close_packed horizontally
55
return ROOT.TGLayoutHints(KNORMAL, close, close, normal, normal)
56
raise ValueError("Failed to recognise layout option "+str(option))
58
class GuiError(Exception):
60
GuiErrot can be thrown when user wants to catch and handle errors that
61
are specifically related to gui errors (and e.g. let other errors pass
64
def __init__(self, *args, **kwargs):
65
"""Initialise Exception base class"""
66
Exception.__init__(self, *args, **kwargs)
71
Wrapper for ROOT TGLabel
73
I couldn't find an elegant way to build a ROOT TGLabel with a particular
74
width; so I fill the label with default characters, and then update once
75
the window is drawn with the actual text. Note that ROOT default font is not
76
fixed width, so fill characters should have some width.
78
def __init__(self, parent, name, label_length, alignment, _layout):
82
- parent; parent ROOT frame
83
- name; text that will fill the label
84
- label_length; width of the label
85
- alignment; integer index defining how the text is aligned within the
89
self.frame = ROOT.TGLabel(parent, "a"*label_length)
90
self.frame.SetTextJustify(alignment)
91
parent.AddFrame(self.frame, layout(_layout))
94
"""Call to update the text once the window is built"""
95
self.frame.SetText(self.name)
98
def new_from_dict(my_dict, parent):
100
Build a Label based on a dictionary
101
- my_dict; dictionary of string:value pairs defining the layout
102
- type; should be "label"
103
- name; text to fill the label
104
- alignment; 'right', 'center' or 'left' to align the text.
105
Vertical alignment is always center
106
- label_length; Integer defining the default width of the label.
107
- layout; see layout function...
108
alignment, label_length, layout are optional
109
- parent; reference to the parent frame
110
Return value is the dictionary with additional entry "frame" appended
111
containing a reference to the TGLabel object
113
if my_dict["type"] != "label":
114
raise ValueError("Not a label")
115
name = my_dict["name"]
116
alignment = ROOT.TGLabel.kTextLeft
117
if "alignment" in my_dict and my_dict["alignment"] == "right":
118
alignment = ROOT.TGLabel.kTextRight
119
elif "alignment" in my_dict and my_dict["alignment"] == "center":
120
alignment = ROOT.TGLabel.kTextCenterX
122
if "label_length" in my_dict:
123
label_length = my_dict["label_length"]
125
my_dict["label_length"] = label_length
127
if "layout" in my_dict:
128
_layout = my_dict["layout"]
130
my_dict["layout"] = _layout
131
my_dict["label"] = Label(parent, name, label_length, alignment, _layout)
132
my_dict["frame"] = my_dict["label"].frame
135
class NamedTextEntry: # pylint: disable=R0913
137
A named text entry is a label followed by a text entry box.
139
These are used frequently enough that it is worthwhile to make a special
142
def __init__(self, parent, name, default_text, entry_length=10, \
143
label_length=5, tooltip=""):
145
Initialise the text entry
146
- parent; parent frame
147
- name; string name that will appear in the label and is used to name
149
- default_text; text that will be placed in the text entry by default
150
- entry_length; width of the text entry field (number of characters)
151
- label_length; width of the label (number of characters)
152
- tooltip; text to place in a tool tip - if empty string, no tool tip
155
self.frame = ROOT.TGHorizontalFrame(parent)
157
self.label = ROOT.TGLabel(self.frame, "a"*label_length)
158
self.label.SetTextJustify(ROOT.TGLabel.kTextLeft)
159
self.text_entry = ROOT.TGTextEntry(self.frame, "a"*entry_length, 0)
160
self.frame.AddFrame(self.label, layout("close_v"))
161
self.frame.AddFrame(self.text_entry, layout("close_v"))
162
self.text_entry.SetText(default_text)
164
self.text_entry.SetToolTipText(tooltip)
168
Update the label to contain actual text
170
self.label.SetText(self.name)
173
def new_from_dict(my_dict, parent):
175
Initialise a text entry from a dictionary
176
- my_dict; dictionary containing configuration options
177
- type; should be string like 'named_text_entry'
178
- name; text to put in the label
179
- default_text; default text that fills the text entry box
180
- tool_tip; make a tool tip next to the text entry
181
- entry_length (optional); width of the text entry box (number of
183
- label_length (optional); width of the label (number of
185
- parent; parent frame
186
Return value is the dictionary with "text_entry" field and "frame" field
187
appended containing references to the NamedTextEntry and the actual
190
if my_dict["type"] != "named_text_entry":
191
raise ValueError("Not a text box")
192
name = my_dict["name"]
193
default = my_dict["default_text"]
194
entry_length = DEFAULT_TEXT_LENGTH
196
if "tool_tip" in my_dict:
197
tool_tip = my_dict["tool_tip"]
198
if "entry_length" in my_dict:
199
entry_length = my_dict["entry_length"]
201
my_dict["entry_length"] = entry_length
203
if "label_length" in my_dict:
204
label_length = my_dict["label_length"]
206
my_dict["label_length"] = label_length
207
my_dict["text_entry"] = NamedTextEntry(parent, name, default,
208
entry_length, label_length, tooltip=tool_tip)
209
my_dict["frame"] = my_dict["text_entry"].frame
212
def function_wrapper(function):
214
Lightweight wrapper to intervene before TPyDispatcher handles python errors
216
return FunctionWrapper(function).execute
218
class FunctionWrapper: #pylint: disable=R0903
220
Function wrapper to intervene before TPyDispatcher handles python errors
222
def __init__(self, function):
224
Lightweight class that adds some error handling
226
self.function = function
228
def execute(self, *args, **keywdargs):
230
Handle the errors from python function calls as I want
233
return self.function(*args, **keywdargs)
234
except: # pylint: disable=W0702, W0142
235
sys.excepthook(*sys.exc_info())
237
class Window(): # pylint: disable=R0201
239
This is the main class in this module. Initialise a window based on a json
240
configuration file and layout the elements.
243
Json configuration should be a dictionary. The top level object requires
245
- type; either main_frame (builds a TGMainFrame) or transient_frame
246
(builds a TGTransientFrmae
248
- children; list of child frames, each of which is a dict
250
Window initialisation iterates over the child dicts and parses each one
251
into a child TGFrame. All frames support a "layout" entry, which
252
determines the amount of padding that will be placed around the entry.
253
- "layout"; set the amount of padding in the frame. Can be either
254
"normal" to make standard padding (5 units in each direction), "close"
255
padding (1 unit in each direction), "close_v" (1 unit vertically, 5
256
horizontally) or "close_h" (1 unit horizontally, 5 vertically)
257
The "type" entry of the dictionary determines the type of TGFrame that
258
will be contructed. Options are:
259
- "horizontal_frame"; makes a TGHorizontalFrame, with a list of children.
260
- "children": list of child frames to place in the horizontal frame
261
Each sub-frame will be placed horizontally next to the previous one.
262
- "vertical_frame"; makes a TGVerticalFrame, with a list of children.
263
- "children": list of child frames to place in the horizontal frame.
264
Each sub-frame will be placed vertically below the previous one.
265
- "label"; makes a text TGLabel
266
- "name": name of the label
267
- "label_length": width of space allocated to the label in characters.
269
- "text_entry"; makes a TGTextEntry
270
- "name": name of the label
271
- "label_length": width of space allocated to the label in characters.
273
- "named_text_entry": makes a NamedTextEntry, which is a collection of
274
TGHorizontalFrame, TGLabel and TGTextEntry. See NamedTextEntry
275
documentation for more details.
276
- "button": makes a TGTextButton i.e. standard click button
277
- "name": text to be used on the button; put an ampersand in to set a
279
- "drop_down": make a TGComboBox i.e. a drop down box.
280
- "entries": list of strings, each of which becomes an entry in the
281
drop down box, indexing from 0
282
- "selected": integer determining the entry that will be selected
283
initialliy (Default 0)
284
- "check_button": make a TGCheckButton i.e. a true/false tick box
285
- "text": text to place next to the check button
286
- "default_state": set to 1 to make the box ticked by default, 0 to
288
- "special": user-defined GUI elements can be added at RunTime by making a
290
- "manipulator": name of the manipulator to use for this special item.
291
manipulators are set at the initialisation stage by passing a
292
dictionary to manipulator_dict. Dictionary should contain a mapping
293
that maps string "manipulator" to the function that will be called
294
at run time to set up the "special" GUI element. The "manipulator"
295
function should take the GUI element dict as an argument and return
296
it at the end. The returned GUI element dict will be used to
297
construct the GUI element as per normal.
300
def __init__(self, parent, main, data_file=None, # pylint: disable = R0913
301
json_data=None, manipulator_dict=None):
303
Initialise the window
304
- parent; parent ROOT frame (e.g. TGMainFrame/TGTransientFrame)
305
- main; top level TGMainFrame for the whole GUI
306
- data_file; string containing the file name for the layout data
307
- json_data; json object containing the layout data
308
- manipulator_dict; if there are any frames of type "special", this
309
dictionary maps to a function which performs the layout for that
311
Throws an exception if both, or neither, of data_file and json_data are
312
defined - caller must define the window layout in one and only one place
314
if data_file != None and json_data != None:
315
raise ValueError("Define only one of json_data and data_file")
316
if data_file == None and json_data == None:
317
raise ValueError("Must define json_data or data_file")
318
if data_file != None:
319
self.data = open(data_file).read()
320
self.data = json.loads(self.data)
322
self.data = json_data
323
if manipulator_dict == None:
324
manipulator_dict = {}
325
self.main_frame = None
326
self.update_list = []
327
self.socket_list = []
328
self.tg_text_entries = {}
329
self.manipulators = manipulator_dict
330
self._setup_frame(parent, main)
333
def close_window(self):
337
# root v5.34 - makes segv if I don't disable the text entry before
339
for tg_text_entry in self.tg_text_entries.values():
340
tg_text_entry.SetState(0)
341
self.main_frame.CloseWindow()
342
self.main_frame = None
344
def get_frame(self, frame_name, frame_type, frame_list=None):
346
Get the frame with given name and type from a frame list
347
- frame_name; specify the "name" field in the frame's dictionary
348
- frame_type; specify the "type" field in the frame's dictionary
349
- frame_list; search within this list. If not defined, will use the
350
top level frames list (self.data)
351
Returns a ROOT TGFrame object. Raises ValueError if no frame was found
353
frame_dict = self.get_frame_dict(frame_name, frame_type, frame_list)
354
return frame_dict["frame"]
356
def get_frame_dict(self, frame_name, frame_type, frame_list=None):
358
Get the frame dictionary with given name and type from a frame list
359
- frame_name; specify the "name" field in the frame's dictionary
360
- frame_type; specify the "type" field in the frame's dictionary
361
- frame_list; search within this list. If not defined, will use the
362
top level frames list (self.data)
363
Returns a dictionary object containing the frame configuration. The ROOT
364
TGFrame object is mapped to the "frame" key.
366
if frame_list == None:
367
frame_list = self.data["children"]
368
frame = self._get_frame_dict_recurse(frame_name, frame_type, frame_list)
370
raise ValueError("Could not find frame of name "+str(frame_name)+\
371
" type "+str(frame_type))
374
def set_action(self, frame_name, frame_type, frame_socket, action):
376
Set an action that will occur on a given signal
377
- frame_name; string containing the name of the frame that makes the
379
- frame_type; string containing the type of the frame that makes the
381
- frame_socket; string containing the name of the ROOT function that
382
makes the signal. If the ROOT function takes arguments, those must
384
- action; function that will be called when the specified signal is
386
e.g. my_window.set_action("select_box", "drop_down", "Selected(Int_t)",
388
will call select_action() when the frame with "type":"drop_down" and
389
"name":"select_box" makes a signal Selected(Int_t).
391
frame = self.get_frame(frame_name, frame_type)
392
self.socket_list.append(ROOT.TPyDispatcher(function_wrapper(action)))
393
frame.Connect(frame_socket, 'TPyDispatcher', self.socket_list[-1],
396
def set_button_action(self, button_name, action):
398
Set an action that will occur when a button emits a Clicked() signal
399
- button_name; name of the button
400
- action; function that will be called when the button is clicked
402
self.set_action(button_name, "button", "Clicked()", action)
404
def get_text_entry(self, name, value_type):
406
Get the text stored in either a text_entry or a named_text_entry
407
- name; name of the text entry
408
- value_type; convert the value to the given type.
409
Returns a value of type value_type.
410
Raises a type error if the conversion fails (string conversion will
413
text_entry, text_length = self._find_text_entry(name)
417
value = value_type(text_entry.GetText())
420
raise GuiError("Failed to parse text entry "+name+" as "+\
423
def set_text_entry(self, name, value):
425
Set the text stored in either a text_entry or a named_text_entry
426
- name; name of the text entry
427
- value; put the given value in the text_entry, converting to a
428
string using the value's __str__ method. The resultant
429
string will be truncated to the width of the text entry.
431
text_entry, text_length = self._find_text_entry(name)
432
my_value = str(value)
433
if len(my_value) > text_length-1:
434
my_value = my_value[0:text_length-1]
435
text_entry.SetText(my_value)
437
def _get_frame_dict_recurse(self, frame_name, frame_type, frame_list=None):
439
(Private) recursively search for a frame of a given name and type.
440
Called by get_frame_dict.
442
for item in frame_list:
443
if "name" in item and "type" in item:
444
if item["name"] == frame_name and item["type"] == frame_type:
446
# if we have not found the frame dict here, then recurse into
448
if "children" in item:
449
frame = self._get_frame_dict_recurse(frame_name,
455
def _find_text_entry(self, name):
457
(Private) find a text entry. Search first for named_text_entry, if that
458
fails search for text_entry. Return the TGTextEntry and the length of
459
the text entry (number of characters).
462
value_dict = self.get_frame_dict(name, 'named_text_entry')
465
entry_length = DEFAULT_TEXT_LENGTH
466
if value_dict != None:
467
if "entry_length" in value_dict:
468
entry_length = value_dict["entry_length"]
469
return (value_dict['text_entry'].text_entry,
471
value_dict = self.get_frame_dict(name, 'text_entry')
472
if "entry_length" in value_dict:
473
entry_length = value_dict["entry_length"]
474
if value_dict == None:
475
raise ValueError("Could not find text entry named "+str(name))
476
return value_dict["frame"], entry_length
478
def _setup_frame(self, parent, main):
480
(Private) set up the frame. Parse the json file and convert to ROOT
481
TGFrames, lay out the windows and so forth.
483
if self.data["type"] == "transient_frame":
484
self.main_frame = ROOT.TGTransientFrame(parent, main)
485
elif self.data["type"] == "main_frame":
486
self.main_frame = ROOT.TGMainFrame(parent)
488
raise ValueError("Failed to recognise frame type "+\
489
str(self.data["type"]))
490
self._expand_frames(self.data["children"], self.main_frame)
491
self.data["frame"] = self.main_frame
492
self.main_frame.SetWindowName(self.data["name"])
493
self.main_frame.MapSubwindows()
494
self.main_frame.Resize(self.main_frame.GetDefaultSize())
495
self.main_frame.MapWindow()
498
def _label_update(self):
500
(Private) once the labels are laid out, with width enforced by fake
501
string data, update the labels with the real text. A bit hacky.
503
for item in self.update_list:
506
def _expand_frames(self, frames, parent):
508
Parse the json object, adding frames to all json items
510
for frames_index, item in enumerate(frames):
511
if item["type"] == "special":
512
manipulator_name = item["manipulator"]
513
if manipulator_name not in self.manipulators:
514
raise ValueError("Manipulator "+manipulator_name+\
515
" has not been defined")
516
item = self.manipulators[manipulator_name](item)
517
frames[frames_index] = item
518
if item["type"] in self.parse_item_dict.keys():
519
parser = self.parse_item_dict[item["type"]]
520
parser(self, parent, item)
522
raise ValueError("Did not recognise item type "+item["type"])
523
layout_option = "normal"
524
if "layout" in item.keys():
525
layout_option = item["layout"]
526
parent.AddFrame(item["frame"], layout(layout_option))
527
if "children" in item:
528
self._expand_frames(item["children"], item["frame"])
530
def _parse_horizontal_frame(self, parent, item):
531
"""parse a horizontal_frame into a TGHorizontalFrame"""
532
item["frame"] = ROOT.TGHorizontalFrame(parent)
534
def _parse_vertical_frame(self, parent, item):
535
"""parse a vertical_frame into a TGVerticalFrame"""
536
item["frame"] = ROOT.TGVerticalFrame(parent)
538
def _parse_named_text_entry(self, parent, item):
539
"""parse a name_text_entry into a NamedEntry"""
540
item = NamedTextEntry.new_from_dict(item, parent)
541
self.tg_text_entries[item["name"]] = item["text_entry"].text_entry
542
self.update_list.append(item["text_entry"])
544
def _parse_canvas(self, parent, item):
545
"""parse a canvas into a TRootEmbeddedCanvas"""
546
item["frame"] = ROOT.TRootEmbeddedCanvas('canvas',
547
parent, item["width"], item["height"])
550
def _parse_label(self, parent, item):
551
"""parse a label into a TGLabel"""
552
item = Label.new_from_dict(item, parent)
553
self.update_list.append(item["label"])
555
def _parse_button(self, parent, item):
556
"""parse a text_button into a TGTextButton"""
558
item["frame"] = ROOT.TGTextButton(parent, name, 50)
560
def _parse_text_entry(self, parent, item):
561
"""parse a text_entry into a TGTextEntry"""
562
entry_length = DEFAULT_TEXT_LENGTH
563
if "entry_length" in item:
564
entry_length = item["entry_length"]
565
item["frame"] = ROOT.TGTextEntry(parent, "a"*entry_length, 0)
566
# use default, then change to get width right?
568
if "default_text" in item:
569
default_text = item["default_text"]
570
item["frame"].SetText(default_text)
571
self.tg_text_entries[item["name"]] = item["frame"]
573
def _parse_drop_down(self, parent, item):
574
"""parse a drop_down into a TGComboBox"""
575
item["frame"] = ROOT.TGComboBox(parent)
576
for i, entry in enumerate(item["entries"]):
577
item["frame"].AddEntry(entry, i)
578
item["frame"].Resize(150, 20)
579
if "selected" in item:
580
item["frame"].Select(item["selected"])
582
def _parse_check_button(self, parent, item):
583
"""parse a check_button into a TGCheckButton"""
584
item["frame"] = ROOT.TGCheckButton(parent, item["text"])
585
item["frame"].SetState(item["default_state"])
588
"horizontal_frame":_parse_horizontal_frame,
589
"vertical_frame":_parse_vertical_frame,
590
"named_text_entry":_parse_named_text_entry,
591
"canvas":_parse_canvas,
592
"label":_parse_label,
593
"button":_parse_button,
594
"text_entry":_parse_text_entry,
595
"drop_down":_parse_drop_down,
596
"check_button":_parse_check_button,