Skip to content

Commit 43d36ca

Browse files
authored
Merge pull request #11 from alustig3/task-menus
Task selection menu
2 parents db0c121 + 6bc24c7 commit 43d36ca

12 files changed

+660
-590
lines changed

com/data_logger.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def copy_task_file(self, data_dir, tasks_dir, dir_name='task_files'):
4747
if not os.path.exists(exp_tasks_dir):
4848
os.mkdir(exp_tasks_dir)
4949
task_file_path = os.path.join(tasks_dir, self.sm_info['name']+'.py')
50-
task_save_name = self.sm_info['name']+'_{}.py'.format(self.sm_info['task_hash'])
50+
task_save_name = os.path.split(self.sm_info['name'])[1] +'_{}.py'.format(self.sm_info['task_hash'])
5151
if not task_save_name in os.listdir(exp_tasks_dir):
5252
copyfile(task_file_path, os.path.join(exp_tasks_dir, task_save_name))
5353

gui/GUI_main.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,19 @@ def view_forum(self):
131131
def view_github(self):
132132
QtGui.QDesktopServices.openUrl(QtCore.QUrl("https://github.com/pyControl/pyControl"))
133133

134+
def get_task_file_list(self):
135+
'''Return list of .py files in tasks folder and subfolders in format:
136+
subdir_1/subdir_2/task_file_name.py'''
137+
task_files = []
138+
for (dirpath, dirnames, filenames) in os.walk(dirs['tasks']):
139+
task_files += [os.path.join(dirpath, file).split(dirs['tasks'])[1][1:-3]
140+
for file in filenames if file.endswith('.py')]
141+
return task_files
142+
134143
def refresh(self):
135144
'''Called regularly when framework not running.'''
136145
# Scan task folder.
137-
tasks = [t.split('.')[0] for t in os.listdir(dirs['tasks']) if t[-3:] == '.py']
146+
tasks = self.get_task_file_list()
138147
self.available_tasks_changed = tasks != self.available_tasks
139148
if self.available_tasks_changed:
140149
self.available_tasks = tasks

gui/configure_experiment_tab.py

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from config.paths import dirs
77
from gui.dialogs import invalid_run_experiment_dialog, invalid_save_experiment_dialog,unrun_subjects_dialog
8-
from gui.utility import TableCheckbox, cbox_update_options, cbox_set_item, null_resize, variable_constants, init_keyboard_shortcuts
8+
from gui.utility import TableCheckbox, cbox_update_options, cbox_set_item, null_resize, variable_constants, init_keyboard_shortcuts,TaskSelectMenu
99

1010
# --------------------------------------------------------------------------------
1111
# Experiments_tab
@@ -46,11 +46,9 @@ def __init__(self, parent=None):
4646
self.name_label = QtGui.QLabel('Experiment name:')
4747
self.name_text = QtGui.QLineEdit()
4848
self.task_label = QtGui.QLabel('Task:')
49-
self.task_select = QtGui.QComboBox()
50-
self.task_select.setFixedWidth(150)
49+
self.task_select = TaskSelectMenu(dirs['tasks'],'select task')
5150
self.hardware_test_label = QtGui.QLabel('Hardware test:')
52-
self.hardware_test_select = QtGui.QComboBox()
53-
self.hardware_test_select.setFixedWidth(150)
51+
self.hardware_test_select = TaskSelectMenu(dirs['tasks'],'no hardware test',add_default=True)
5452
self.data_dir_label = QtGui.QLabel('Data dir:')
5553
self.data_dir_text = QtGui.QLineEdit(dirs['data'])
5654
self.data_dir_button = QtGui.QPushButton('')
@@ -87,19 +85,17 @@ def __init__(self, parent=None):
8785
self.variables_groupbox = QtGui.QGroupBox('Variables')
8886
self.variablesbox_layout = QtGui.QHBoxLayout(self.variables_groupbox)
8987
self.variables_table = VariablesTable(self)
88+
self.task_select.set_callback(self.variables_table.task_changed)
9089
self.variablesbox_layout.addWidget(self.variables_table)
9190

9291
# Initialise widgets
9392
self.experiment_select.addItems(['select experiment'])
94-
self.task_select.addItems(['select task'])
95-
self.hardware_test_select.addItems(['no hardware test'])
9693

9794
# Connect signals.
9895
self.name_text.textChanged.connect(self.name_edited)
9996
self.data_dir_text.textEdited.connect(lambda: setattr(self, 'custom_dir', True))
10097
self.data_dir_button.clicked.connect(self.select_data_dir)
10198
self.experiment_select.activated[str].connect(self.experiment_changed)
102-
self.task_select.activated[str].connect(self.variables_table.task_changed)
10399
self.new_button.clicked.connect(lambda: self.new_experiment(dialog=True))
104100
self.delete_button.clicked.connect(self.delete_experiment)
105101
self.save_button.clicked.connect(self.save_experiment)
@@ -140,8 +136,8 @@ def experiment_changed(self, experiment_name):
140136
def refresh(self):
141137
'''Called periodically when not running to update available task, ports, experiments.'''
142138
if self.GUI_main.available_tasks_changed:
143-
cbox_update_options(self.task_select, self.GUI_main.available_tasks)
144-
cbox_update_options(self.hardware_test_select, ['no hardware test'] + self.GUI_main.available_tasks)
139+
self.task_select.update_menu()
140+
self.hardware_test_select.update_menu()
145141
self.GUI_main.available_tasks_changed = False
146142
if self.GUI_main.available_experiments_changed:
147143
cbox_update_options(self.experiment_select, self.GUI_main.available_experiments)
@@ -157,11 +153,11 @@ def refresh(self):
157153
if (str(self.name_text.text()) == '') and not self.custom_dir:
158154
self.data_dir_text.setText(dirs['data'])
159155

160-
def experiment_dict(self,filtered = False):
156+
def experiment_dict(self, filtered=False):
161157
'''Return the current state of the experiments tab as a dictionary.'''
162158
return {'name': self.name_text.text(),
163-
'task': str(self.task_select.currentText()),
164-
'hardware_test': str(self.hardware_test_select.currentText()),
159+
'task': str(self.task_select.text()),
160+
'hardware_test': str(self.hardware_test_select.text()),
165161
'data_dir': self.data_dir_text.text(),
166162
'subjects': self.subjects_table.subjects_dict(filtered),
167163
'variables': self.variables_table.variables_list(),
@@ -177,8 +173,8 @@ def new_experiment(self, dialog=True):
177173
self.subjects_table.reset()
178174
self.variables_table.reset()
179175
cbox_set_item(self.experiment_select, 'select experiment', insert=True)
180-
cbox_set_item(self.task_select, 'select task', insert=True)
181-
cbox_set_item(self.hardware_test_select, 'no hardware test', insert=True)
176+
self.task_select.setText('select task')
177+
self.hardware_test_select.setText('no hardware test')
182178
self.subset_warning_checkbox.setChecked(True)
183179
self.saved_exp_dict = self.experiment_dict()
184180
self.saved_exp_path = None
@@ -238,8 +234,14 @@ def load_experiment(self, experiment_name):
238234
with open(exp_path,'r') as exp_file:
239235
experiment = json.loads(exp_file.read())
240236
self.name_text.setText(experiment['name'])
241-
cbox_set_item(self.task_select, experiment['task'])
242-
cbox_set_item(self.hardware_test_select, experiment['hardware_test'])
237+
if experiment['task'] in self.GUI_main.available_tasks:
238+
self.task_select.setText(experiment['task'])
239+
else:
240+
self.task_select.setText('select task')
241+
if experiment['hardware_test'] in self.GUI_main.available_tasks:
242+
self.hardware_test_select.setText(experiment['hardware_test'])
243+
else:
244+
self.hardware_test_select.setText('no hardware test')
243245
cbox_set_item(self.experiment_select, experiment['name'])
244246
self.subset_warning_checkbox.setChecked(experiment['subset_warning'])
245247
self.variables_table.task_changed(experiment['task'])
@@ -254,7 +256,6 @@ def run_experiment(self):
254256
'''Check that the experiment is valid. Prompt user to save experiment if
255257
it is new or has been edited. Then run experiment.'''
256258
experiment = self.experiment_dict(filtered=True)
257-
258259
if not experiment['name']:
259260
invalid_run_experiment_dialog(self, 'Experiment must have a name.')
260261
return

gui/run_task_tab.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from gui.dialogs import Variables_dialog
1414
from gui.plotting import Task_plot
15-
from gui.utility import init_keyboard_shortcuts
15+
from gui.utility import init_keyboard_shortcuts,TaskSelectMenu
1616

1717
# Run_task_gui ------------------------------------------------------------------------
1818

@@ -105,7 +105,8 @@ def __init__(self, parent=None):
105105
self.task_groupbox = QtGui.QGroupBox('Task')
106106

107107
self.task_label = QtGui.QLabel('Task:')
108-
self.task_select = QtGui.QComboBox()
108+
self.task_select = TaskSelectMenu(dirs['tasks'],'select task')
109+
self.task_select.set_callback(self.task_changed)
109110
self.upload_button = QtGui.QPushButton('Upload')
110111
self.upload_button.setIcon(QtGui.QIcon("gui/icons/circle-arrow-up.svg"))
111112
self.variables_button = QtGui.QPushButton('Variables')
@@ -118,7 +119,6 @@ def __init__(self, parent=None):
118119
self.taskgroup_layout.addWidget(self.variables_button)
119120
self.task_groupbox.setLayout(self.taskgroup_layout)
120121

121-
self.task_select.currentTextChanged.connect(self.task_changed)
122122
self.upload_button.clicked.connect(self.setup_task)
123123

124124
# Session groupbox.
@@ -175,7 +175,7 @@ def __init__(self, parent=None):
175175
# Keyboard Shortcuts
176176

177177
shortcut_dict = {
178-
't' : lambda: self.task_select.showPopup(),
178+
't' : lambda: self.task_select.showMenu(),
179179
'u' : lambda: self.setup_task(),
180180
'Space' : (lambda: self.stop_task() if self.running
181181
else self.start_task() if self.uploaded else None)
@@ -219,8 +219,7 @@ def refresh(self):
219219
else: # No setups available to connect to.
220220
self.connect_button.setEnabled(False)
221221
if self.GUI_main.available_tasks_changed:
222-
self.task_select.clear()
223-
self.task_select.addItems(sorted(self.GUI_main.available_tasks))
222+
self.task_select.update_menu()
224223
if self.GUI_main.data_dir_changed and not self.custom_dir:
225224
self.data_dir_text.setText(dirs['data'])
226225
if self.task:
@@ -287,15 +286,17 @@ def disconnect(self):
287286
self.task_changed()
288287
self.connected = False
289288

290-
def task_changed(self):
289+
def task_changed(self,task=None):
291290
self.uploaded = False
292291
self.upload_button.setText('Upload')
293292
self.upload_button.setIcon(QtGui.QIcon("gui/icons/circle-arrow-up.svg"))
294293
self.start_button.setEnabled(False)
295294

296295
def setup_task(self):
296+
task = self.task_select.text()
297+
if task == 'select task':
298+
return
297299
try:
298-
task = self.task_select.currentText()
299300
if self.uploaded:
300301
self.status_text.setText('Resetting task..')
301302
else:

gui/utility.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from pyqtgraph.Qt import QtGui, QtCore, QtWidgets
2+
import os
23

34
# --------------------------------------------------------------------------------
45
# GUI utility functions, classes, variables.
@@ -291,3 +292,61 @@ def dropEvent(self, event):
291292
'''
292293
self.dragDropedPos = event.pos()
293294
QtWidgets.QTabBar.dropEvent(self, event)
295+
296+
# ----------------------------------------------------------------------------------
297+
# TaskSelectMenu
298+
# ----------------------------------------------------------------------------------
299+
300+
class TaskSelectMenu(QtGui.QPushButton):
301+
'''Nested menu used to select tasks. The menu items are the names of
302+
any .py files in root_folder and it's sub-directories. Items are
303+
nested in the menu according to the sub-directory structure.
304+
initial_text is shown before anything is selected, and if add_default
305+
is True, initial_text is included as a menu option.
306+
Adapted from: https://stackoverflow.com/questions/35924235
307+
'''
308+
def __init__(self, root_folder, initial_text, add_default=False):
309+
self.callback = lambda task: None
310+
self.menu = QtGui.QMenu()
311+
self.menu_root = root_folder
312+
self.add_default = add_default
313+
self.default_text = initial_text
314+
super().__init__(initial_text)
315+
316+
def set_callback(self,callback_fxn):
317+
self.callback = callback_fxn
318+
319+
def create_action(self,text):
320+
def fxn():
321+
if self.text() != text:
322+
self.callback(text)
323+
self.setText(text)
324+
return fxn
325+
326+
def update_menu(self):
327+
self.menu.clear()
328+
if self.add_default:
329+
self.menu.addAction(self.default_text,self.create_action(self.default_text))
330+
self.menu.addSeparator()
331+
previous_menu = self.menu
332+
current_menu = self.menu
333+
for dirName, subdirList, fileList in os.walk(self.menu_root):
334+
subfolder = dirName.split(self.menu_root)[1][1:]
335+
if subfolder:
336+
if any(".py" in filename for filename in fileList): # only add submenu if there are .py files inside
337+
sub_menu = current_menu.addMenu(subfolder.split(os.path.sep)[-1])
338+
for filename in fileList:
339+
if filename.endswith('.py'):
340+
menuItem = filename[:-3]
341+
sub_menu.addAction(menuItem,self.create_action(os.path.join(subfolder,menuItem)))
342+
if subdirList: # continue down to next level
343+
previous_menu = current_menu
344+
current_menu = sub_menu
345+
else: # return up to previous level
346+
current_menu = previous_menu
347+
else: # list top level files
348+
for filename in fileList:
349+
if filename.endswith('.py'):
350+
menuItem = filename[:-3]
351+
self.menu.addAction(menuItem,self.create_action(menuItem))
352+
self.setMenu(self.menu)

0 commit comments

Comments
 (0)