Skip to content

Commit 6e5320f

Browse files
committed
- Added dialog to allow users to change the path of the data and tasks dir which is accessed from new settings menu. User path settings are saved as a json object in 'config/user_paths.json' which is not version controlled.
- All folder paths are now stored in a single 'dirs' dictionary defined in config.paths rather than as seperate variables (e.g. task_dir, data_dir).
1 parent e6c853f commit 6e5320f

File tree

7 files changed

+149
-44
lines changed

7 files changed

+149
-44
lines changed

config/paths.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,29 @@
11
import os
2+
import json
3+
4+
# Default paths.
25

36
top_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Top level pyControl folder.
47

5-
config_dir = os.path.join(top_dir, 'config')
6-
framework_dir = os.path.join(top_dir, 'pyControl')
7-
devices_dir = os.path.join(top_dir, 'devices')
8-
tasks_dir = os.path.join(top_dir, 'tasks')
9-
experiments_dir = os.path.join(top_dir, 'experiments')
10-
data_dir = os.path.join(top_dir, 'data')
11-
transfer_dir = None # Folder to copy data to at end of run experiment.
8+
dirs = {
9+
'config' : os.path.join(top_dir, 'config'),
10+
'framework' : os.path.join(top_dir, 'pyControl'),
11+
'devices' : os.path.join(top_dir, 'devices'),
12+
'tasks' : os.path.join(top_dir, 'tasks'),
13+
'experiments' : os.path.join(top_dir, 'experiments'),
14+
'data' : os.path.join(top_dir, 'data')
15+
}
16+
17+
# User paths - When paths.py is imported on opening GUI, load any
18+
# saved user paths and update the dirs dict.
19+
20+
def update_paths(user_paths):
21+
for name, path in user_paths.items():
22+
if os.path.exists(path):
23+
dirs[name] = path
1224

25+
json_path = os.path.join(dirs['config'], 'user_paths.json')
26+
if os.path.exists(json_path):
27+
with open(json_path,'r') as f:
28+
user_paths = json.loads(f.read())
29+
update_paths(user_paths)

gui/GUI_main.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
from serial.tools import list_ports
66
from pyqtgraph.Qt import QtGui, QtCore
77

8-
from config.paths import tasks_dir, experiments_dir, data_dir
8+
from config.paths import dirs
99
from config.gui_settings import VERSION
1010
from gui.run_task_tab import Run_task_tab
11-
from gui.dialogs import Board_config_dialog, Keyboard_shortcuts_dialog
11+
from gui.dialogs import Board_config_dialog, Keyboard_shortcuts_dialog, Paths_dialog
1212
from gui.configure_experiment_tab import Configure_experiment_tab
1313
from gui.run_experiment_tab import Run_experiment_tab
1414
from gui.setups_tab import Setups_tab
@@ -32,13 +32,15 @@ def __init__(self):
3232
self.available_tasks_changed = False
3333
self.available_experiments_changed = False
3434
self.available_ports_changed = False
35+
self.data_dir_changed = False
3536
self.current_tab_ind = 0 # Which tab is currently selected.
3637
self.app = None # Overwritten with QtGui.QApplication instance in main.
3738

3839
# Dialogs.
3940

4041
self.config_dialog = Board_config_dialog(parent=self)
4142
self.shortcuts_dialog = Keyboard_shortcuts_dialog(parent=self)
43+
self.paths_dialog = Paths_dialog(parent=self)
4244

4345
# Widgets.
4446
self.tab_widget = QtGui.QTabWidget(self)
@@ -77,12 +79,18 @@ def __init__(self):
7779
data_action = QtGui.QAction("&Data", self)
7880
data_action.setShortcut("Ctrl+D")
7981
data_action.triggered.connect(self.go_to_data)
80-
# View Task Directory
8182
folders_menu.addAction(data_action)
83+
# View Task Directory
8284
task_action = QtGui.QAction("&Tasks", self)
8385
task_action.setShortcut("Ctrl+T")
8486
task_action.triggered.connect(self.go_to_tasks)
8587
folders_menu.addAction(task_action)
88+
## --------Settings menu--------
89+
settings_menu = main_menu.addMenu('Settings')
90+
# Folder paths
91+
paths_action = QtGui.QAction("&Folder paths", self)
92+
paths_action.triggered.connect(self.paths_dialog.exec)
93+
settings_menu.addAction(paths_action)
8694
# ---------Help menu----------
8795
help_menu= main_menu.addMenu('Help')
8896
# Go to readthedocs
@@ -105,10 +113,10 @@ def __init__(self):
105113
self.show()
106114

107115
def go_to_data(self):
108-
QtGui.QDesktopServices.openUrl(QtCore.QUrl.fromLocalFile(data_dir))
116+
QtGui.QDesktopServices.openUrl(QtCore.QUrl.fromLocalFile(dirs['data']))
109117

110118
def go_to_tasks(self):
111-
QtGui.QDesktopServices.openUrl(QtCore.QUrl.fromLocalFile(tasks_dir))
119+
QtGui.QDesktopServices.openUrl(QtCore.QUrl.fromLocalFile(dirs['tasks']))
112120

113121
def view_docs(self):
114122
QtGui.QDesktopServices.openUrl(QtCore.QUrl("https://pycontrol.readthedocs.io/en/latest/"))
@@ -122,12 +130,12 @@ def view_github(self):
122130
def refresh(self):
123131
'''Called regularly when framework not running.'''
124132
# Scan task folder.
125-
tasks = [t.split('.')[0] for t in os.listdir(tasks_dir) if t[-3:] == '.py']
133+
tasks = [t.split('.')[0] for t in os.listdir(dirs['tasks']) if t[-3:] == '.py']
126134
self.available_tasks_changed = tasks != self.available_tasks
127135
if self.available_tasks_changed:
128136
self.available_tasks = tasks
129137
# Scan experiments folder.
130-
experiments = [t.split('.')[0] for t in os.listdir(experiments_dir) if t[-4:] == '.pcx']
138+
experiments = [t.split('.')[0] for t in os.listdir(dirs['experiments']) if t[-4:] == '.pcx']
131139
self.available_experiments_changed = experiments != self.available_experiments
132140
if self.available_experiments_changed:
133141
self.available_experiments = experiments
@@ -141,6 +149,8 @@ def refresh(self):
141149
self.run_task_tab.refresh()
142150
self.configure_experiment_tab.refresh()
143151
self.setups_tab.refresh()
152+
# Clear flags.
153+
self.data_dir_changed = False
144154

145155
def tab_changed(self, new_tab_ind):
146156
'''Called whenever the active tab is changed.'''

gui/configure_experiment_tab.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import json
44
from pyqtgraph.Qt import QtGui, QtCore
55

6-
from config.paths import data_dir, tasks_dir, experiments_dir
6+
from config.paths import dirs
77
from gui.dialogs import invalid_experiment_dialog
88
from gui.utility import TableCheckbox, cbox_update_options, cbox_set_item, null_resize, variable_constants, init_keyboard_shortcuts
99

@@ -20,7 +20,7 @@ def __init__(self, parent=None):
2020

2121
# Variables
2222
self.GUI_main = self.parent()
23-
self.custom_dir = False # True if data_dir field has been manually edited.
23+
self.custom_dir = False # True if data_dir field has been manually edited.
2424
self.saved_exp_path = None # Path of last saved/loaded experiment file.
2525
self.saved_exp_dict = {} # Dict of last saved/loaded experiment.
2626

@@ -50,7 +50,7 @@ def __init__(self, parent=None):
5050
self.hardware_test_select = QtGui.QComboBox()
5151
self.hardware_test_select.setFixedWidth(150)
5252
self.data_dir_label = QtGui.QLabel('Data dir:')
53-
self.data_dir_text = QtGui.QLineEdit(data_dir)
53+
self.data_dir_text = QtGui.QLineEdit(dirs['data'])
5454
self.data_dir_button = QtGui.QPushButton('...')
5555
self.data_dir_button.setFixedWidth(30)
5656

@@ -115,12 +115,13 @@ def __init__(self, parent=None):
115115

116116
def name_edited(self):
117117
if not self.custom_dir:
118-
self.data_dir_text.setText(os.path.join(data_dir, self.name_text.text()))
118+
self.data_dir_text.setText(os.path.join(dirs['data'], self.name_text.text()))
119119

120120
def select_data_dir(self):
121-
self.data_dir_text.setText(
122-
QtGui.QFileDialog.getExistingDirectory(self, 'Select data folder', data_dir))
123-
self.custom_dir = True
121+
new_path = QtGui.QFileDialog.getExistingDirectory(self, 'Select data folder', dirs['data'])
122+
if new_path:
123+
self.data_dir_text.setText(new_path)
124+
self.custom_dir = True
124125

125126
def experiment_changed(self, experiment_name):
126127
if experiment_name in self.GUI_main.available_experiments:
@@ -143,6 +144,11 @@ def refresh(self):
143144
self.subjects_table.update_available_setups()
144145
if self.saved_exp_dict != self.experiment_dict():
145146
self.save_button.setEnabled(True)
147+
else:
148+
self.save_button.setEnabled(False)
149+
if self.GUI_main.data_dir_changed:
150+
if (str(self.name_text.text()) == '') and not self.custom_dir:
151+
self.data_dir_text.setText(dirs['data'])
146152

147153
def experiment_dict(self):
148154
'''Return the current state of the experiments tab as a dictionary.'''
@@ -158,7 +164,7 @@ def new_experiment(self, dialog=True):
158164
if dialog:
159165
if not self.save_dialog(): return
160166
self.name_text.setText('')
161-
self.data_dir_text.setText(data_dir)
167+
self.data_dir_text.setText(dirs['data'])
162168
self.custom_dir = False
163169
self.subjects_table.reset()
164170
self.variables_table.reset()
@@ -170,7 +176,7 @@ def new_experiment(self, dialog=True):
170176

171177
def delete_experiment(self):
172178
'''Delete an experiment file after dialog to confirm deletion.'''
173-
exp_path = os.path.join(experiments_dir, self.name_text.text()+'.pcx')
179+
exp_path = os.path.join(dirs['experiments'], self.name_text.text()+'.pcx')
174180
if os.path.exists(exp_path):
175181
reply = QtGui.QMessageBox.question(self, 'Delete experiment',
176182
"Delete experiment '{}'".format(self.name_text.text()),
@@ -184,7 +190,7 @@ def save_experiment(self, from_dialog=False):
184190
saved in the experiments folder as .pcx file.'''
185191
experiment = self.experiment_dict()
186192
file_name = self.name_text.text()+'.pcx'
187-
exp_path = os.path.join(experiments_dir, file_name)
193+
exp_path = os.path.join(dirs['experiments'], file_name)
188194
if os.path.exists(exp_path) and (exp_path != self.saved_exp_path):
189195
reply = QtGui.QMessageBox.question(self, 'Replace file',
190196
"File '{}' already exists, do you want to replace it?".format(file_name),
@@ -201,7 +207,7 @@ def save_experiment(self, from_dialog=False):
201207

202208
def load_experiment(self, experiment_name):
203209
'''Load experiment .pcx file and set fields of experiment tab.'''
204-
exp_path = os.path.join(experiments_dir, experiment_name +'.pcx')
210+
exp_path = os.path.join(dirs['experiments'], experiment_name +'.pcx')
205211
with open(exp_path,'r') as exp_file:
206212
experiment = json.loads(exp_file.read())
207213
self.name_text.setText(experiment['name'])
@@ -275,7 +281,7 @@ def save_dialog(self):
275281
cancel is selected, True otherwise.'''
276282
if self.saved_exp_dict == self.experiment_dict():
277283
return True # Experiment has not been edited.
278-
exp_path = os.path.join(experiments_dir, self.name_text.text()+'.pcx')
284+
exp_path = os.path.join(dirs['experiments'], self.name_text.text()+'.pcx')
279285
dialog_text = None
280286
if not os.path.exists(exp_path):
281287
dialog_text = 'Experiment not saved, save experiment?'
@@ -513,7 +519,7 @@ def task_changed(self, task):
513519
'''Remove variables that are not defined in the new task.'''
514520
pattern = "[\n\r]v\.(?P<vname>\w+)\s*\="
515521
try:
516-
with open(os.path.join(tasks_dir, task+'.py'), "r") as file:
522+
with open(os.path.join(dirs['tasks'], task+'.py'), "r") as file:
517523
file_content = file.read()
518524
except FileNotFoundError:
519525
return

gui/dialogs.py

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import os
2+
import json
23

34
from pyqtgraph.Qt import QtGui, QtCore
45

5-
from config.paths import config_dir
6+
from config.paths import dirs, update_paths
67
from gui.utility import variable_constants
78

89
# Board_config_dialog -------------------------------------------------
@@ -44,7 +45,7 @@ def load_framework(self):
4445

4546
def load_hardware_definition(self):
4647
hwd_path = QtGui.QFileDialog.getOpenFileName(self, 'Select hardware definition:',
47-
os.path.join(config_dir, 'hardware_definition.py'), filter='*.py')[0]
48+
os.path.join(dirs['config'], 'hardware_definition.py'), filter='*.py')[0]
4849
self.accept()
4950
self.board.load_hardware_definition(hwd_path)
5051

@@ -231,4 +232,70 @@ def __init__(self, parent):
231232
label.setFont(QtGui.QFont('Helvetica', 10))
232233
self.Vlayout.addWidget(label)
233234

234-
self.setFixedSize(self.sizeHint())
235+
self.setFixedSize(self.sizeHint())
236+
237+
# Paths dialog. ---------------------------------------------------------
238+
239+
class Path_setter():
240+
def __init__(self, name, path, edited, dialog):
241+
self.name = name
242+
self.path = os.path.normpath(path)
243+
self.edited = edited
244+
self.dialog = dialog
245+
# Instantiate widgets
246+
self.name_label = QtGui.QLabel(name +' folder:')
247+
self.path_text = QtGui.QLineEdit(self.path)
248+
self.path_text.setReadOnly(True)
249+
self.path_text.setFixedWidth(400)
250+
self.change_button = QtGui.QPushButton('Change')
251+
self.change_button.clicked.connect(self.select_path)
252+
# Layout
253+
self.hlayout = QtGui.QHBoxLayout()
254+
self.hlayout.addWidget(self.name_label)
255+
self.hlayout.addWidget(self.path_text)
256+
self.hlayout.addWidget(self.change_button)
257+
self.dialog.Vlayout.addLayout(self.hlayout)
258+
259+
self.dialog.setters.append(self)
260+
261+
def select_path(self):
262+
new_path = QtGui.QFileDialog.getExistingDirectory(
263+
self.dialog, 'Select {} folder'.format(self.name), self.path)
264+
if new_path:
265+
new_path = os.path.normpath(new_path)
266+
if new_path != self.path:
267+
self.path = new_path
268+
self.edited = True
269+
self.path_text.setText(new_path)
270+
271+
class Paths_dialog(QtGui.QDialog):
272+
'''Dialog for displaying information about keyboard shortcuts.'''
273+
def __init__(self, parent):
274+
super(QtGui.QDialog, self).__init__(parent)
275+
self.setWindowTitle('Paths')
276+
277+
self.Vlayout = QtGui.QVBoxLayout(self)
278+
self.setters = []
279+
280+
# Instantiate setters
281+
self.tasks_setter = Path_setter('tasks', dirs['tasks'], False, self)
282+
self.data_setter = Path_setter('data' , dirs['data'] , False, self)
283+
284+
self.setFixedSize(self.sizeHint())
285+
286+
def closeEvent(self, event):
287+
'''Save any user edited paths as json in config folder.'''
288+
edited_paths = {s.name: s.path for s in self.setters if s.edited}
289+
if edited_paths:
290+
# Store newly edited paths.
291+
json_path = os.path.join(dirs['config'],'user_paths.json')
292+
if os.path.exists(json_path):
293+
with open(json_path,'r') as f:
294+
user_paths = json.loads(f.read())
295+
else:
296+
user_paths = {}
297+
user_paths.update(edited_paths)
298+
with open(json_path, 'w') as f:
299+
f.write(json.dumps(user_paths))
300+
self.parent().data_dir_changed = True
301+
update_paths(user_paths)

gui/run_experiment_tab.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from serial import SerialException
99

1010
from config.gui_settings import update_interval
11-
from config.paths import tasks_dir
11+
from config.paths import dirs
1212
from com.pycboard import Pycboard, PyboardError
1313
from com.data_logger import Data_logger
1414
from gui.plotting import Experiment_plot
@@ -187,7 +187,7 @@ def setup_experiment(self, experiment):
187187
self.abort_experiment()
188188
return
189189
# Copy task file to experiments data folder.
190-
self.boards[0].data_logger.copy_task_file(self.experiment['data_dir'], tasks_dir)
190+
self.boards[0].data_logger.copy_task_file(self.experiment['data_dir'], dirs['tasks'])
191191
# Configure GUI ready to run.
192192
for i, board in enumerate(self.boards):
193193
self.subjectboxes[i].assign_board(board)

0 commit comments

Comments
 (0)