Skip to content

Commit 5e567a1

Browse files
committed
GUI updates and finding Workbench
1 parent 06a588d commit 5e567a1

File tree

10 files changed

+173
-56
lines changed

10 files changed

+173
-56
lines changed

package/build_force

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ function find_raven {
5757
# Function to find RAVEN plugins given a list of possible locations
5858
function find_plugin {
5959
for loc in ${LOCATIONS[@]} $RAVEN_LOC; do
60-
for dir in $(find $loc/plugins -maxdepth 1 -type d -iname $1); do
60+
for dir in $(find $loc -maxdepth 3 -type d -iname $1 2> /dev/null); do
6161
echo $(realpath $dir)
6262
return
6363
done
@@ -125,7 +125,10 @@ echo " ... RAVEN location: $RAVEN_LOC"
125125
echo " ... HERON location: $HERON_LOC"
126126
echo " ... TEAL location: $TEAL_LOC"
127127
echo " ... Destination: $SCRIPT_DIR/force_install/docs"
128-
sh $SCRIPT_DIR/make_docs --raven-dir $RAVEN_LOC --heron-dir $HERON_LOC --teal-dir $TEAL_LOC --dest $SCRIPT_DIR/force_install/docs
128+
# The --no-build flag is used to avoid building the documentation. This is because the documentation
129+
# requires activatation of the raven_libraries conda environment, otherwise the HERON documentation
130+
# build will fail. Rebuild the documentation before building the FORCE package!
131+
sh $SCRIPT_DIR/make_docs --raven-dir $RAVEN_LOC --heron-dir $HERON_LOC --teal-dir $TEAL_LOC --dest $SCRIPT_DIR/force_install/docs --no-build
129132

130133
# Copy over relevant examples
131134
echo "Copying over the FORCE examples"

package/copy_examples

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,13 @@ for ex in ${EXAMPLES[@]}; do
4444
done
4545

4646
# Clean up the copied examples, removing files and directories created when running the tests.
47-
FILES_TO_REMOVE=("tests" "moped_input.xml" "outer.xml" "inner.xml" "cashflow.xml")
47+
FILES_TO_REMOVE=("tests" "moped_input.xml" "outer.xml" "inner.xml" "cash.xml" "*.lib")
4848
DIRS_TO_REMOVE=("__pycache__" "gold" "*_o")
4949
for filename in ${FILES_TO_REMOVE[@]}; do
50-
find $EXAMPLES_DIR -name $filename -exec rm {} \;
50+
find $EXAMPLES_DIR -name $filename -exec rm {} \; 2>/dev/null
5151
done
5252
for dirname in ${DIRS_TO_REMOVE[@]}; do
53-
find $EXAMPLES_DIR -type d -name $dirname -exec rm -r {} \;
53+
find $EXAMPLES_DIR -type d -name $dirname -exec rm -r {} \; 2>/dev/null
5454
done
5555

5656
# If building on Mac, replace the %HERON_DATA% magic string with a relative path to the data
@@ -84,12 +84,25 @@ if [[ "$OSTYPE" == "darwin"* ]]; then
8484
WORKBENCH_APP=$(find /Applications -type d -name "Workbench-*.app" | head -n 1)
8585
XML2EDDI=$(realpath $WORKBENCH_APP/Contents/rte/util/xml2eddi.py)
8686
else
87-
# Use the readlink command to get the full path to the Workbench executable
88-
WORKBENCH_BIN=$(dirname $(readlink -f $(which Workbench)))
89-
XML2EDDI=$(realpath $WORKBENCH_BIN/../rte/util/xml2eddi.py)
87+
if ! command -v Workbench &> /dev/null; then
88+
# If Workbench isn't in the system PATH. Look in typical installation locations for Workbench to
89+
# find the xml2eddi.py script. Common locatoins are /c/Workbench-5.4.1/ and $HOME/Workbench-5.4.1/.
90+
if [ -d "/c/Workbench-5.4.1" ]; then
91+
XML2EDDI="/c/Workbench-5.4.1/rte/util/xml2eddi.py"
92+
elif [ -d "$HOME/Workbench-5.4.1" ]; then
93+
XML2EDDI="$HOME/Workbench-5.4.1/rte/util/xml2eddi.py"
94+
else
95+
XML2EDDI="" # Workbench not found
96+
fi
97+
else
98+
# Workbench is in the system PATH. Use the location of the Workbench executable to find the xml2eddi.py script.
99+
XML2EDDI=$(realpath $(command -v Workbench)/../rte/util/xml2eddi.py)
100+
fi
90101
fi
91-
if [ -x "$XML2EDDI" ]; then
102+
103+
# If XML2EDDI is not empty, convert the HERON workshop tests to .heron files
104+
if [ -n "$XML2EDDI" ]; then
92105
for ex in $(find $EXAMPLES_DIR/workshop -name "heron_input*.xml"); do
93-
python $XML2EDDI $ex
106+
python $XML2EDDI $ex > ${ex%.xml}.heron
94107
done
95108
fi

package/inno_package.iss

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ Name: "{autoprograms}\FORCE\examples"; Filename: "{app}\examples"
5151
Name: "{autodesktop}\HERON"; Filename: "{app}\heron.exe"; Tasks: desktopicon
5252
Name: "{autodesktop}\RAVEN"; Filename: "{app}\raven_framework.exe"; Tasks: desktopicon
5353
Name: "{autodesktop}\TEAL"; Filename: "{app}\teal.exe"; Tasks: desktopicon
54+
; Add desktop icons for the documentation and examples directories
55+
Name: "{autodesktop}\FORCE Documentation"; Filename: "{app}\docs"; Tasks: desktopicon
56+
Name: "{autodesktop}\FORCE Examples"; Filename: "{app}\examples"; Tasks: desktopicon
5457

5558
[Registry]
5659
; File association for .heron files
@@ -95,6 +98,7 @@ procedure CurStepChanged(CurStep: TSetupStep);
9598
var
9699
DefaultAppsFilePath: string;
97100
DefaultAppsContent: string;
101+
WorkbenchConfigPath: string;
98102
ResultCode: Integer;
99103
begin
100104
if (CurStep = ssPostInstall) and (WorkbenchPath <> '') then
@@ -108,7 +112,7 @@ begin
108112
// Associate .heron files with the Workbench executable
109113
RegWriteStringValue(HKEY_CURRENT_USER, 'Software\Classes\FORCE.heron\shell\open\command', '', '"' + WorkbenchPath + '" "%1"');
110114
111-
DefaultAppsFilePath := ExtractFilePath(WorkbenchPath) + 'default.apps.json';
115+
DefaultAppsFilePath := ExtractFilePath(WorkbenchPath) + 'default.apps.son';
112116
DefaultAppsContent :=
113117
'applications {' + #13#10 +
114118
' HERON {' + #13#10 +
@@ -124,9 +128,16 @@ begin
124128
' }' + #13#10 +
125129
' }';
126130
131+
// Save the default.apps.son file in the Workbench base directory
127132
if not SaveStringToFile(DefaultAppsFilePath, DefaultAppsContent, False) then
128133
begin
129-
MsgBox('Failed to create default.apps.json in the Workbench directory.', mbError, MB_OK);
134+
MsgBox('Failed to create default.apps.son in the Workbench directory.', mbError, MB_OK);
135+
end;
136+
137+
// Save the path to the Workbench executable in a file at {app}/.workbench.
138+
if not SaveStringToFile(ExpandConstant('{app}') + '\.workbench', WorkbenchPath, False) then
139+
begin
140+
MsgBox('Failed to save the path to the Workbench executable.', mbError, MB_OK);
130141
end;
131142
end;
132143
end;

package/make_docs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,16 +74,19 @@ echo "FORCE documentation directory: $DOC_DIR"
7474
mkdir -p "$DOC_DIR"
7575

7676
# Build the documentation for the FORCE tools
77-
for loc in RAVEN_DIR HERON_DIR TEAL_DIR; do
77+
# for loc in RAVEN_DIR HERON_DIR TEAL_DIR; do
78+
for loc in HERON_DIR TEAL_DIR; do
7879
pushd "${!loc}/doc" > /dev/null
80+
echo $(pwd)
7981

8082
# If the build flag is set, build the documentation.
8183
if [ $NO_BUILD -eq 0 ]; then
8284
echo "Building documentation for $(basename ${!loc})"
83-
# Either a Makefile or a make_docs.sh script should be present in the doc directory
84-
if [ -f Makefile ]; then
85+
if [[ -f "Makefile" ]] && command -v "make" >/dev/null 2>&1; then
8586
make
86-
elif [ -f make_docs.sh ]; then
87+
elif [[ -f "make_docs.bat" ]] && [[ $OSTYPE == "msys" ]]; then
88+
./make_docs.bat
89+
elif [[ -f "make_docs.sh" ]]; then
8790
bash make_docs.sh
8891
else
8992
echo "ERROR: No Makefile or make_docs.sh script found in $(basename ${!loc}) doc directory."

package/ui/controllers/file_selection.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
from typing import Optional
22
import os
3-
import tkinter as tk
43
import argparse
5-
from collections import namedtuple
64

75
from .file_location_persistence import FileLocationPersistence
86
from .file_dialog import FileDialogController

package/ui/models/main.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from typing import Callable
33

44

5+
56
class Model:
67
""" Runs a function in a separate thread """
78
def __init__(self, func: Callable, **kwargs):

package/ui/utils.py

Lines changed: 114 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,152 @@
1-
import xml.etree.ElementTree as ET
21
import os
2+
import pathlib
33
import subprocess
44
import platform
55
import shutil
6+
import tkinter as tk
7+
from tkinter import messagebox, filedialog
68

79

8-
def check_parallel(path) -> bool:
10+
def find_workbench() -> pathlib.Path:
911
"""
10-
Checks if a ravenframework input file uses parallel processing. This poses a problem for the executable
11-
on MacOS and possibly Linux.
12-
13-
@In, path, str, path to the input file
14-
@Out, is_parallel, bool, True if parallel processing is used, False otherwise
12+
Finds the NEAMS Workbench executable. A ".workbench" file in the FORCE app's main directory tracks
13+
the location of the Workbench executable. If that file doesn't exist, we look in common directories
14+
to find Workbench ourselves. If we still can't find it, we ask the user to locate it manually, if
15+
desired.
1516
"""
16-
is_parallel = False
17-
18-
tree = ET.parse(path)
19-
tree.find()
20-
21-
return is_parallel
17+
workbench_path = None
2218

19+
# Check if a ".workbench" file exists in the main build directory (same directory as the heron and
20+
# raven_framework executables). That should be 2 directories up from the directory of this file.
21+
current_file_dir = pathlib.Path(__file__).parent
22+
workbench_file = current_file_dir.parent.parent / ".workbench"
23+
workbench_file_exists = workbench_file.exists()
24+
if workbench_file_exists:
25+
with open(workbench_file, 'r') as f:
26+
workbench_path = f.read().strip()
27+
if not os.path.exists(workbench_path):
28+
# If the path in the .workbench file is invalid, delete the file so we don't keep trying
29+
# to use it.
30+
workbench_path = None
31+
os.remove(workbench_file)
32+
else:
33+
return workbench_path
2334

24-
def find_workbench():
25-
"""
26-
Finds the NEAMS Workbench executable
27-
"""
28-
workbench_path = None
35+
# If that .workbench file doesn't exist, we can look around for the Workbench executable
2936
if platform.system() == "Windows":
30-
if os.environ.get('WORKBENCH_PATH'):
31-
workbench_path = os.environ['WORKBENCH_PATH']
37+
if wb_path := os.environ.get('WORKBENCH_PATH', None):
38+
workbench_path = wb_path
39+
elif wb_path := shutil.which('Workbench'):
40+
workbench_path = wb_path
3241
else:
33-
workbench_path = shutil.which('workbench')
42+
# Manually search through a few common directories for the Workbench installation
43+
for path in ["$HOMEDRIVE\\", "$PROGRAMFILES", "$HOME", "$APPDATA", "$LOCALAPPDATA"]:
44+
path = os.path.expandvars(path)
45+
if not os.path.exists(path):
46+
continue
47+
for file in os.listdir(path):
48+
if file.startswith("Workbench"):
49+
wb_path = os.path.join(path, file, "bin", "Workbench.exe")
50+
if os.path.exists(wb_path):
51+
workbench_path = wb_path
52+
break
3453
elif platform.system() == "Darwin":
3554
# Look in the /Applications directory for a directory starting with "Workbench"
3655
for app in os.listdir("/Applications"):
3756
if app.startswith("Workbench") and os.path:
3857
workbench_path = os.path.join("/Applications", app, "Contents/MacOS/Workbench")
3958
break
40-
else:
41-
print("ERROR: Could not find the NEAMS Workbench application in the /Applications directory. "
42-
"Has Workbench been installed?")
43-
else: # Linux, not yet supported
44-
print("Automatic connection of FORCE tools to the NEAMS Workbench is not yet supported on Linux.")
59+
60+
# If we still haven't found Workbench, let the user help us out. Throw up a tkinter warning dialog to
61+
# ask the user to locate the Workbench executable.
62+
if workbench_path is None:
63+
root = tk.Tk()
64+
root.withdraw()
65+
66+
response = messagebox.askyesno(
67+
"NEAMS Workbench could not be found!",
68+
"The NEAMS Workbench executable could not be found. Would you like to locate it manually?"
69+
)
70+
if response:
71+
workbench_path = filedialog.askopenfilename(
72+
title="Locate NEAMS Workbench",
73+
filetypes=[("Workbench Executable", "*.exe")]
74+
)
75+
if workbench_path:
76+
with open(workbench_file, 'w') as f:
77+
f.write(workbench_path)
78+
79+
if isinstance(workbench_path, str):
80+
workbench_path = pathlib.Path(workbench_path)
81+
4582
return workbench_path
4683

4784

85+
def create_workbench_heron_default(workbench_path: pathlib.Path):
86+
"""
87+
Creates a configuration file for Workbench so it knows where HERON is located.
88+
@ In, workbench_path, pathlib.Path, the path to the NEAMS Workbench executable
89+
"""
90+
# First, we need to find the HERON executable. This will be "heron.exe" on Windows
91+
# and just "heron" on MacOS and Linux. It should be located 2 directories up from the
92+
# directory of this file.
93+
current_file_dir = pathlib.Path(__file__).parent
94+
heron_path = current_file_dir.parent.parent / "heron"
95+
if platform.system() == "Windows":
96+
heron_path = heron_path.with_suffix(".exe")
97+
# If the HERON executable doesn't exist, we can't create the Workbench configuration file
98+
if not heron_path.exists():
99+
print(f"ERROR: Could not find the HERON executable in the directory {heron_path.parent}.")
100+
return
101+
102+
# Create the configuration file for Workbench
103+
workbench_root_dir = workbench_path.parent.parent
104+
workbench_config_file = workbench_root_dir / "default.apps.son"
105+
if workbench_config_file.exists():
106+
# File already exists, but does a HERON entry already exist? See if HERON mentioned in the
107+
# file. This isn't as robust as actually parsing the file, but it should work for now.
108+
with open(workbench_config_file, 'r') as f:
109+
for line in f:
110+
if "heron" in line.lower():
111+
return
112+
# If the file doesn't exist or doesn't mention HERON, we need to add it
113+
print("Adding HERON configuration to NEAMS Workbench", workbench_config_file)
114+
with open(workbench_config_file, 'a') as f:
115+
f.write("\napplications {\n"
116+
" HERON {\n"
117+
" configurations {\n"
118+
" default {\n"
119+
" options {\n"
120+
" shared {\n"
121+
f" \"Executable\"=\"{heron_path}\"\n"
122+
" }\n"
123+
" }\n"
124+
" }\n"
125+
" }\n"
126+
" }\n"
127+
"}\n")
128+
129+
48130
def run_in_workbench(file: str | None = None):
49131
"""
50132
Opens the given file in the NEAMS Workbench
51133
@ In, file, str, optional, the file to open in NEAMS Workbench
52134
"""
53-
# Find the workbench executable
135+
# Find the Workbench executable
54136
workbench_path = find_workbench()
55137
if workbench_path is None:
56138
print("ERROR: Could not find the NEAMS Workbench executable. Please set the "
57139
"WORKBENCH_PATH environment variable or add it to the system path.")
58140
raise RuntimeError
59141

60-
# Open the file in the workbench
61-
command = workbench_path
142+
# Point Workbench to the HERON executable if it's not already configured
143+
create_workbench_heron_default(workbench_path)
144+
145+
# Open the file in Workbench
62146
# Currently, we're only able to open the MacOS version of Workbench by opening the app itself.
63147
# This does not accept a file as an argument, so users will need to open Workbench, then open
64148
# the desired file manually from within the app.
149+
command = str(workbench_path)
65150
if file is not None and platform.system() == "Windows":
66151
command += ' ' + file
67152

package/ui/views/file_selection.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,10 @@ def __init__(self,
5252
super().__init__(master)
5353
self.file_title = tk.Label(self, text=label)
5454
self.file_title.grid(row=0, column=0, columnspan=2, sticky='w')
55-
self.browse_button = tk.Button(self, text='Browse')
55+
self.browse_button = tk.Button(self, text='Browse', width=10, padx=5)
5656
self.browse_button.grid(row=1, column=0, sticky='w')
5757
self.filename = tk.StringVar()
5858
self.filename.set("Select a file") # Default filename is "Select a file", i.e. no file selected
59-
self.filename_label = tk.Label(self, textvariable=self.filename)
60-
self.filename_label.grid(row=1, column=1, sticky='w')
59+
self.filename_label = tk.Label(self, textvariable=self.filename, bg="white", anchor='w', padx=10, pady=3)
60+
self.filename_label.grid(row=1, column=1, sticky='w', padx=5)
6161
self.grid_columnconfigure(1, weight=1)

package/ui/views/run_abort.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ def __init__(self, master, **kwargs):
1111
@Out, None
1212
"""
1313
super().__init__(master, **kwargs)
14-
self.abort_button = tk.Button(self, text='Abort')
15-
self.abort_button.grid(row=0, column=0, sticky='w')
14+
button_width = 10
15+
self.abort_button = tk.Button(self, text='Abort', width=button_width)
16+
self.abort_button.grid(row=0, column=0, sticky='w', padx=5)
1617

17-
self.run_button = tk.Button(self, text='Run')
18-
self.run_button.grid(row=0, column=1, sticky='w')
18+
self.run_button = tk.Button(self, text='Run', width=button_width)
19+
self.run_button.grid(row=0, column=1, sticky='w', padx=5)
1920

20-
self.grid_columnconfigure(1, weight=1)
21+
self.grid_columnconfigure(0, minsize=50)
22+
self.grid_columnconfigure(1, weight=1, minsize=50)

package/ui/views/text_output.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ def __init__(self, master, **kwargs):
1212
@Out, None
1313
"""
1414
super().__init__(master, **kwargs)
15-
self.show_hide_button = tk.Button(self, text='Hide Ouptut')
15+
self.show_hide_button = tk.Button(self, text='Hide Ouptut', pady=5, width=15)
1616
self.show_hide_button.grid(row=0, column=0, sticky='w')
1717
self.text = ScrolledText(self, state=tk.DISABLED)
1818
self.is_showing = True # To use with show/hide button
1919
self.text.grid(row=1, column=0, sticky='nsew')
20+
self.grid_rowconfigure(0, minsize=50)
2021
self.grid_rowconfigure(1, weight=1)
2122
self.grid_columnconfigure(0, weight=1)

0 commit comments

Comments
 (0)