Skip to content

Commit

Permalink
GUI updates and finding Workbench
Browse files Browse the repository at this point in the history
  • Loading branch information
j-bryan committed Jul 3, 2024
1 parent 06a588d commit 5e567a1
Show file tree
Hide file tree
Showing 10 changed files with 173 additions and 56 deletions.
7 changes: 5 additions & 2 deletions package/build_force
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ function find_raven {
# Function to find RAVEN plugins given a list of possible locations
function find_plugin {
for loc in ${LOCATIONS[@]} $RAVEN_LOC; do
for dir in $(find $loc/plugins -maxdepth 1 -type d -iname $1); do
for dir in $(find $loc -maxdepth 3 -type d -iname $1 2> /dev/null); do
echo $(realpath $dir)
return
done
Expand Down Expand Up @@ -125,7 +125,10 @@ echo " ... RAVEN location: $RAVEN_LOC"
echo " ... HERON location: $HERON_LOC"
echo " ... TEAL location: $TEAL_LOC"
echo " ... Destination: $SCRIPT_DIR/force_install/docs"
sh $SCRIPT_DIR/make_docs --raven-dir $RAVEN_LOC --heron-dir $HERON_LOC --teal-dir $TEAL_LOC --dest $SCRIPT_DIR/force_install/docs
# The --no-build flag is used to avoid building the documentation. This is because the documentation
# requires activatation of the raven_libraries conda environment, otherwise the HERON documentation
# build will fail. Rebuild the documentation before building the FORCE package!
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

# Copy over relevant examples
echo "Copying over the FORCE examples"
Expand Down
29 changes: 21 additions & 8 deletions package/copy_examples
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@ for ex in ${EXAMPLES[@]}; do
done

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

# If building on Mac, replace the %HERON_DATA% magic string with a relative path to the data
Expand Down Expand Up @@ -84,12 +84,25 @@ if [[ "$OSTYPE" == "darwin"* ]]; then
WORKBENCH_APP=$(find /Applications -type d -name "Workbench-*.app" | head -n 1)
XML2EDDI=$(realpath $WORKBENCH_APP/Contents/rte/util/xml2eddi.py)
else
# Use the readlink command to get the full path to the Workbench executable
WORKBENCH_BIN=$(dirname $(readlink -f $(which Workbench)))
XML2EDDI=$(realpath $WORKBENCH_BIN/../rte/util/xml2eddi.py)
if ! command -v Workbench &> /dev/null; then
# If Workbench isn't in the system PATH. Look in typical installation locations for Workbench to
# find the xml2eddi.py script. Common locatoins are /c/Workbench-5.4.1/ and $HOME/Workbench-5.4.1/.
if [ -d "/c/Workbench-5.4.1" ]; then
XML2EDDI="/c/Workbench-5.4.1/rte/util/xml2eddi.py"
elif [ -d "$HOME/Workbench-5.4.1" ]; then
XML2EDDI="$HOME/Workbench-5.4.1/rte/util/xml2eddi.py"
else
XML2EDDI="" # Workbench not found
fi
else
# Workbench is in the system PATH. Use the location of the Workbench executable to find the xml2eddi.py script.
XML2EDDI=$(realpath $(command -v Workbench)/../rte/util/xml2eddi.py)
fi
fi
if [ -x "$XML2EDDI" ]; then

# If XML2EDDI is not empty, convert the HERON workshop tests to .heron files
if [ -n "$XML2EDDI" ]; then
for ex in $(find $EXAMPLES_DIR/workshop -name "heron_input*.xml"); do
python $XML2EDDI $ex
python $XML2EDDI $ex > ${ex%.xml}.heron
done
fi
15 changes: 13 additions & 2 deletions package/inno_package.iss
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ Name: "{autoprograms}\FORCE\examples"; Filename: "{app}\examples"
Name: "{autodesktop}\HERON"; Filename: "{app}\heron.exe"; Tasks: desktopicon
Name: "{autodesktop}\RAVEN"; Filename: "{app}\raven_framework.exe"; Tasks: desktopicon
Name: "{autodesktop}\TEAL"; Filename: "{app}\teal.exe"; Tasks: desktopicon
; Add desktop icons for the documentation and examples directories
Name: "{autodesktop}\FORCE Documentation"; Filename: "{app}\docs"; Tasks: desktopicon
Name: "{autodesktop}\FORCE Examples"; Filename: "{app}\examples"; Tasks: desktopicon

[Registry]
; File association for .heron files
Expand Down Expand Up @@ -95,6 +98,7 @@ procedure CurStepChanged(CurStep: TSetupStep);
var
DefaultAppsFilePath: string;
DefaultAppsContent: string;
WorkbenchConfigPath: string;
ResultCode: Integer;
begin
if (CurStep = ssPostInstall) and (WorkbenchPath <> '') then
Expand All @@ -108,7 +112,7 @@ begin
// Associate .heron files with the Workbench executable
RegWriteStringValue(HKEY_CURRENT_USER, 'Software\Classes\FORCE.heron\shell\open\command', '', '"' + WorkbenchPath + '" "%1"');
DefaultAppsFilePath := ExtractFilePath(WorkbenchPath) + 'default.apps.json';
DefaultAppsFilePath := ExtractFilePath(WorkbenchPath) + 'default.apps.son';
DefaultAppsContent :=
'applications {' + #13#10 +
' HERON {' + #13#10 +
Expand All @@ -124,9 +128,16 @@ begin
' }' + #13#10 +
' }';
// Save the default.apps.son file in the Workbench base directory
if not SaveStringToFile(DefaultAppsFilePath, DefaultAppsContent, False) then
begin
MsgBox('Failed to create default.apps.json in the Workbench directory.', mbError, MB_OK);
MsgBox('Failed to create default.apps.son in the Workbench directory.', mbError, MB_OK);
end;
// Save the path to the Workbench executable in a file at {app}/.workbench.
if not SaveStringToFile(ExpandConstant('{app}') + '\.workbench', WorkbenchPath, False) then
begin
MsgBox('Failed to save the path to the Workbench executable.', mbError, MB_OK);
end;
end;
end;
11 changes: 7 additions & 4 deletions package/make_docs
Original file line number Diff line number Diff line change
Expand Up @@ -74,16 +74,19 @@ echo "FORCE documentation directory: $DOC_DIR"
mkdir -p "$DOC_DIR"

# Build the documentation for the FORCE tools
for loc in RAVEN_DIR HERON_DIR TEAL_DIR; do
# for loc in RAVEN_DIR HERON_DIR TEAL_DIR; do
for loc in HERON_DIR TEAL_DIR; do
pushd "${!loc}/doc" > /dev/null
echo $(pwd)

# If the build flag is set, build the documentation.
if [ $NO_BUILD -eq 0 ]; then
echo "Building documentation for $(basename ${!loc})"
# Either a Makefile or a make_docs.sh script should be present in the doc directory
if [ -f Makefile ]; then
if [[ -f "Makefile" ]] && command -v "make" >/dev/null 2>&1; then
make
elif [ -f make_docs.sh ]; then
elif [[ -f "make_docs.bat" ]] && [[ $OSTYPE == "msys" ]]; then
./make_docs.bat
elif [[ -f "make_docs.sh" ]]; then
bash make_docs.sh
else
echo "ERROR: No Makefile or make_docs.sh script found in $(basename ${!loc}) doc directory."
Expand Down
2 changes: 0 additions & 2 deletions package/ui/controllers/file_selection.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
from typing import Optional
import os
import tkinter as tk
import argparse
from collections import namedtuple

from .file_location_persistence import FileLocationPersistence
from .file_dialog import FileDialogController
Expand Down
1 change: 1 addition & 0 deletions package/ui/models/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import Callable



class Model:
""" Runs a function in a separate thread """
def __init__(self, func: Callable, **kwargs):
Expand Down
143 changes: 114 additions & 29 deletions package/ui/utils.py
Original file line number Diff line number Diff line change
@@ -1,67 +1,152 @@
import xml.etree.ElementTree as ET
import os
import pathlib
import subprocess
import platform
import shutil
import tkinter as tk
from tkinter import messagebox, filedialog


def check_parallel(path) -> bool:
def find_workbench() -> pathlib.Path:
"""
Checks if a ravenframework input file uses parallel processing. This poses a problem for the executable
on MacOS and possibly Linux.
@In, path, str, path to the input file
@Out, is_parallel, bool, True if parallel processing is used, False otherwise
Finds the NEAMS Workbench executable. A ".workbench" file in the FORCE app's main directory tracks
the location of the Workbench executable. If that file doesn't exist, we look in common directories
to find Workbench ourselves. If we still can't find it, we ask the user to locate it manually, if
desired.
"""
is_parallel = False

tree = ET.parse(path)
tree.find()

return is_parallel
workbench_path = None

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

def find_workbench():
"""
Finds the NEAMS Workbench executable
"""
workbench_path = None
# If that .workbench file doesn't exist, we can look around for the Workbench executable
if platform.system() == "Windows":
if os.environ.get('WORKBENCH_PATH'):
workbench_path = os.environ['WORKBENCH_PATH']
if wb_path := os.environ.get('WORKBENCH_PATH', None):
workbench_path = wb_path
elif wb_path := shutil.which('Workbench'):
workbench_path = wb_path
else:
workbench_path = shutil.which('workbench')
# Manually search through a few common directories for the Workbench installation
for path in ["$HOMEDRIVE\\", "$PROGRAMFILES", "$HOME", "$APPDATA", "$LOCALAPPDATA"]:
path = os.path.expandvars(path)
if not os.path.exists(path):
continue
for file in os.listdir(path):
if file.startswith("Workbench"):
wb_path = os.path.join(path, file, "bin", "Workbench.exe")
if os.path.exists(wb_path):
workbench_path = wb_path
break
elif platform.system() == "Darwin":
# Look in the /Applications directory for a directory starting with "Workbench"
for app in os.listdir("/Applications"):
if app.startswith("Workbench") and os.path:
workbench_path = os.path.join("/Applications", app, "Contents/MacOS/Workbench")
break
else:
print("ERROR: Could not find the NEAMS Workbench application in the /Applications directory. "
"Has Workbench been installed?")
else: # Linux, not yet supported
print("Automatic connection of FORCE tools to the NEAMS Workbench is not yet supported on Linux.")

# If we still haven't found Workbench, let the user help us out. Throw up a tkinter warning dialog to
# ask the user to locate the Workbench executable.
if workbench_path is None:
root = tk.Tk()
root.withdraw()

response = messagebox.askyesno(
"NEAMS Workbench could not be found!",
"The NEAMS Workbench executable could not be found. Would you like to locate it manually?"
)
if response:
workbench_path = filedialog.askopenfilename(
title="Locate NEAMS Workbench",
filetypes=[("Workbench Executable", "*.exe")]
)
if workbench_path:
with open(workbench_file, 'w') as f:
f.write(workbench_path)

if isinstance(workbench_path, str):
workbench_path = pathlib.Path(workbench_path)

return workbench_path


def create_workbench_heron_default(workbench_path: pathlib.Path):
"""
Creates a configuration file for Workbench so it knows where HERON is located.
@ In, workbench_path, pathlib.Path, the path to the NEAMS Workbench executable
"""
# First, we need to find the HERON executable. This will be "heron.exe" on Windows
# and just "heron" on MacOS and Linux. It should be located 2 directories up from the
# directory of this file.
current_file_dir = pathlib.Path(__file__).parent
heron_path = current_file_dir.parent.parent / "heron"
if platform.system() == "Windows":
heron_path = heron_path.with_suffix(".exe")
# If the HERON executable doesn't exist, we can't create the Workbench configuration file
if not heron_path.exists():
print(f"ERROR: Could not find the HERON executable in the directory {heron_path.parent}.")
return

# Create the configuration file for Workbench
workbench_root_dir = workbench_path.parent.parent
workbench_config_file = workbench_root_dir / "default.apps.son"
if workbench_config_file.exists():
# File already exists, but does a HERON entry already exist? See if HERON mentioned in the
# file. This isn't as robust as actually parsing the file, but it should work for now.
with open(workbench_config_file, 'r') as f:
for line in f:
if "heron" in line.lower():
return
# If the file doesn't exist or doesn't mention HERON, we need to add it
print("Adding HERON configuration to NEAMS Workbench", workbench_config_file)
with open(workbench_config_file, 'a') as f:
f.write("\napplications {\n"
" HERON {\n"
" configurations {\n"
" default {\n"
" options {\n"
" shared {\n"
f" \"Executable\"=\"{heron_path}\"\n"
" }\n"
" }\n"
" }\n"
" }\n"
" }\n"
"}\n")


def run_in_workbench(file: str | None = None):
"""
Opens the given file in the NEAMS Workbench
@ In, file, str, optional, the file to open in NEAMS Workbench
"""
# Find the workbench executable
# Find the Workbench executable
workbench_path = find_workbench()
if workbench_path is None:
print("ERROR: Could not find the NEAMS Workbench executable. Please set the "
"WORKBENCH_PATH environment variable or add it to the system path.")
raise RuntimeError

# Open the file in the workbench
command = workbench_path
# Point Workbench to the HERON executable if it's not already configured
create_workbench_heron_default(workbench_path)

# Open the file in Workbench
# Currently, we're only able to open the MacOS version of Workbench by opening the app itself.
# This does not accept a file as an argument, so users will need to open Workbench, then open
# the desired file manually from within the app.
command = str(workbench_path)
if file is not None and platform.system() == "Windows":
command += ' ' + file

Expand Down
6 changes: 3 additions & 3 deletions package/ui/views/file_selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ def __init__(self,
super().__init__(master)
self.file_title = tk.Label(self, text=label)
self.file_title.grid(row=0, column=0, columnspan=2, sticky='w')
self.browse_button = tk.Button(self, text='Browse')
self.browse_button = tk.Button(self, text='Browse', width=10, padx=5)
self.browse_button.grid(row=1, column=0, sticky='w')
self.filename = tk.StringVar()
self.filename.set("Select a file") # Default filename is "Select a file", i.e. no file selected
self.filename_label = tk.Label(self, textvariable=self.filename)
self.filename_label.grid(row=1, column=1, sticky='w')
self.filename_label = tk.Label(self, textvariable=self.filename, bg="white", anchor='w', padx=10, pady=3)
self.filename_label.grid(row=1, column=1, sticky='w', padx=5)
self.grid_columnconfigure(1, weight=1)
12 changes: 7 additions & 5 deletions package/ui/views/run_abort.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ def __init__(self, master, **kwargs):
@Out, None
"""
super().__init__(master, **kwargs)
self.abort_button = tk.Button(self, text='Abort')
self.abort_button.grid(row=0, column=0, sticky='w')
button_width = 10
self.abort_button = tk.Button(self, text='Abort', width=button_width)
self.abort_button.grid(row=0, column=0, sticky='w', padx=5)

self.run_button = tk.Button(self, text='Run')
self.run_button.grid(row=0, column=1, sticky='w')
self.run_button = tk.Button(self, text='Run', width=button_width)
self.run_button.grid(row=0, column=1, sticky='w', padx=5)

self.grid_columnconfigure(1, weight=1)
self.grid_columnconfigure(0, minsize=50)
self.grid_columnconfigure(1, weight=1, minsize=50)
3 changes: 2 additions & 1 deletion package/ui/views/text_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ def __init__(self, master, **kwargs):
@Out, None
"""
super().__init__(master, **kwargs)
self.show_hide_button = tk.Button(self, text='Hide Ouptut')
self.show_hide_button = tk.Button(self, text='Hide Ouptut', pady=5, width=15)
self.show_hide_button.grid(row=0, column=0, sticky='w')
self.text = ScrolledText(self, state=tk.DISABLED)
self.is_showing = True # To use with show/hide button
self.text.grid(row=1, column=0, sticky='nsew')
self.grid_rowconfigure(0, minsize=50)
self.grid_rowconfigure(1, weight=1)
self.grid_columnconfigure(0, weight=1)

0 comments on commit 5e567a1

Please sign in to comment.