Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ class LcDialog {
Map<String, Stage> resultWindows = [:] // app/tool -> its last results window (Replace reuses it)
Map<String, String> imageChoiceAtRun = [:] // image parameter -> what was chosen when the run started
Map<String, Node> wrappers = [:] // the node placed in the form for each parameter (enabled_when disables this one)
String lastCopied // the text the last Copy as command put on the clipboard (for tests)
Map<String, ComboBox<String>> imageBoxes = [:]
Map<String, String> imageOf = [:] // pixel-size parameter -> image parameter
LcWorker worker
Expand Down Expand Up @@ -278,7 +279,13 @@ class LcDialog {
rescan.onAction = { rescan() }
def restart = new Button("Restart worker")
restart.onAction = { stopWorker(true); status.text = "worker stopped" }
def buttons = new HBox(6, cancelButton, rescan, restart, detailsButton)
def copyMenu = new MenuButton("Copy as command")
copyMenu.tooltip = new Tooltip("Copy what repeats this run outside QuPath: a terminal line or a Python snippet")
def terminalItem = new MenuItem("Terminal command"), pythonItem = new MenuItem("Python snippet")
terminalItem.onAction = { copyAsCommand("terminal") }
pythonItem.onAction = { copyAsCommand("python") }
copyMenu.items.addAll(terminalItem, pythonItem)
def buttons = new HBox(6, cancelButton, rescan, restart, detailsButton, copyMenu)
def scroll = new ScrollPane(formBox)
scroll.fitToWidth = true
scroll.prefViewportHeight = 420
Expand Down Expand Up @@ -729,6 +736,72 @@ class LcDialog {
} catch (Exception ignored) { /* leave what is there */ }
}

// ---- Copy as command (same text as labconstrictor_tools.command)
static final Map<String, String> FILE_PLACEHOLDERS = [image: "image.tif", labels: "labels.tif", table: "table.csv", file: "file", folder: "folder"]

static String shellQuote(String text, boolean windows) {
if (windows) return (text && text ==~ /[A-Za-z0-9_.:\/\\=+,-]+/) ? text : '"' + text.replace('"', '\\"') + '"'
return (text && text ==~ /[A-Za-z0-9_@%+=:,.\/-]+/) ? text : "'" + text.replace("'", "'\"'\"'") + "'"
}

/** The values a command line needs, in the tool's order: the file behind each image, else a placeholder; unset parameters are left out. */
Map commandValues() {
def values = [:], missing = [], notes = []
for (p in currentTool.inputs) {
def v = null
try { v = getters[p.name]() } catch (Exception ignored) { }
if (p.type in ["image", "labels"]) {
def path = null
if (v instanceof Map) {
if (v.file) path = v.file as String
else if (v.source != null) {
try {
def uri = v.source.call().getURIs()?.find { it.scheme == "file" }
if (uri != null) path = new File(uri).path
} catch (Exception ignored) { }
}
if (p.pick_channel && v.channel != null && v.channel >= 0) notes << ("# " + p.name + ": QuPath sent only channel " + (v.channel + 1) + "; the command sends the whole file")
}
v = path
}
if (v != null) values[p.name] = v
if (p.type in FILE_PLACEHOLDERS.keySet() && p.required && values[p.name] == null) { values[p.name] = FILE_PLACEHOLDERS[p.type]; missing << p.name }
}
return [values: values, missing: missing, notes: notes]
}

String commandText(String kind) {
def built = commandValues()
def app = apps[appBox.value]
def head = (built.missing ? ["# replace the file for: " + built.missing.join(", ")] : []) + built.notes
def note = head ? head.join("\n") + "\n" : ""
def given = currentTool.inputs.findAll { built.values.containsKey(it.name) }
if (kind == "python") {
def literal = { v -> v instanceof Boolean ? (v ? "True" : "False") : v instanceof Number ? v.toString() : "'" + v.toString().replace("\\", "\\\\").replace("'", "\\'") + "'" }
def body = given ? "{\n" + given.collect { " '" + it.name + "': " + literal(built.values[it.name]) + "," }.join("\n") + "\n}" : "{}"
return note + "from labconstrictor_tools import client\n\ntask = client.run_once('" + appBox.value + "', '" + currentTool.id + "', " + body + ")\n" +
'print(task.status, task.outputs if task.status == "COMPLETE" else task.error)'
}
boolean windows = System.getProperty("os.name").toLowerCase().contains("win")
def parts = [app.entry.python as String, "-m", "labconstrictor_tools", "run", appBox.value as String, currentTool.id as String].collect { shellQuote(it, windows) }
given.each { p ->
def v = built.values[p.name]
parts << shellQuote(p.name + "=" + (v instanceof Boolean ? (v ? "true" : "false") : v.toString()), windows)
}
return note + parts.join(" ")
}

String copyAsCommand(String kind) {
if (currentTool == null) return null
def text = commandText(kind)
def content = new javafx.scene.input.ClipboardContent()
content.putString(text)
javafx.scene.input.Clipboard.systemClipboard.setContent(content)
lastCopied = text
status.text = "copied the " + (kind == "python" ? "Python snippet" : "terminal command") + " to the clipboard"
return text
}

// ---- running
void run() {
if (running || currentTool == null) return
Expand Down
69 changes: 69 additions & 0 deletions tests/gui_test_copy_body.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@

// ================================================================== GUI test body (appended to the tool script by tests/run_gui_test.sh)
import javafx.embed.swing.SwingFXUtils
import javax.imageio.ImageIO
import java.util.concurrent.CountDownLatch
import java.awt.image.BufferedImage
import javafx.stage.Window

def OUT = new File(System.getProperty("lc.test.out", "/tmp/qp_shots")); OUT.mkdirs()
def FX = "/home/tester/q/fx"
def results = [:]
def fx = { Closure c ->
if (Platform.isFxApplicationThread()) return c()
def latch = new CountDownLatch(1); def box = [null, null]
Platform.runLater { try { box[0] = c() } catch (Throwable t) { box[1] = t } finally { latch.countDown() } }
latch.await()
if (box[1] != null) throw box[1]
return box[0]
}
def expect = { String name, boolean ok, Object detail = "" ->
results[name] = ok
println((ok ? "PASS " : "FAIL ") + name + (ok ? "" : " -> " + detail))
}
def shot = { String name, Stage st ->
Thread.sleep(600)
fx {
def img = st.scene.snapshot(null)
ImageIO.write(SwingFXUtils.fromFXImage(img, null), "png", new File(OUT, name + ".png"))
}
println "SHOT " + name
}
def waitIdle = { dlg, int seconds = 300 ->
def end = System.currentTimeMillis() + seconds * 1000
while (System.currentTimeMillis() < end) { Thread.sleep(200); if (!dlg.running && fx { !dlg.runButton.disable }) return true }
return false
}
def status = { dlg -> fx { dlg.status.text } }
def pick = { dlg, app, tool -> fx { dlg.appBox.value = app }; fx { dlg.toolBox.value = tool } }
def resultStages = { Window.getWindows().findAll { it instanceof Stage && it.showing && it.title?.contains(": ") && it.title != "LabConstrictor: last run" } }


Thread.start("lc-gui-test") {
try {
fx {
def dir = new File("/tmp/qp_project_copy"); dir.deleteDir()
qupath.lib.gui.prefs.PathPrefs.imageTypeSettingProperty().set(qupath.lib.gui.prefs.PathPrefs.ImageTypeSetting.AUTO_ESTIMATE)
def project = qupath.lib.projects.Projects.createProject(dir, BufferedImage)
def server = qupath.lib.images.servers.ImageServers.buildServer("/home/user/LabConstrictor-Fiji/tests/fixtures/sample.tif")
def entry = project.addImage(server.getBuilder()); entry.setImageName("sample.tif")
project.syncChanges(); qupath.setProject(project); qupath.openImageEntry(entry); entry
}
Thread.sleep(2500)
def dlg = fx { lcShow() }
Thread.sleep(800)
pick(dlg, "interactions", "Find bright spots"); Thread.sleep(1500)
fx { dlg.setters["threshold"](0.5d) }
def radios = fx { dlg.wrappers["look_for"].children.findAll { it instanceof RadioButton } }
fx { radios.find { it.text == "dark" }.fire() }
def line = fx { dlg.copyAsCommand("terminal") }
expect("terminal_line_names_app_tool_and_values", line != null && line.contains("run interactions find_bright_spots") && line.contains("threshold=0.5") && line.contains("look_for=dark"), line)
expect("image_file_is_used", line != null && line.contains("image=/home/user/LabConstrictor-Fiji/tests/fixtures/sample.tif"), line)
expect("clipboard_holds_the_line", fx { javafx.scene.input.Clipboard.systemClipboard.getString() } == line, line)
def snippet = fx { dlg.copyAsCommand("python") }
expect("python_snippet_has_real_values", snippet != null && snippet.contains("client.run_once('interactions', 'find_bright_spots'") && snippet.contains("'look_for': 'dark'") && snippet.contains("'threshold': 0.5"), snippet)
new File("/tmp/gtc/qp_copied_line.txt").text = line
shot("qp_copy_1_form", dlg.stage)
} catch (Throwable t) { t.printStackTrace() }
finally { println "RESULTS " + results; Thread.sleep(500); System.exit(0) }
}