fix(paths): centralize file:// URL handling to fix broken save-dir paths
CI / preflight (push) Waiting to run
CI / preflight (push) Waiting to run
This commit is contained in:
@@ -43,7 +43,7 @@ Popup {
|
|||||||
id: saveDirDialog
|
id: saveDirDialog
|
||||||
title: "Choose a folder to save data"
|
title: "Choose a folder to save data"
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
var dir = decodeURIComponent(String(selectedFolder).replace(/^file:\/\//, ""))
|
var dir = String(selectedFolder)
|
||||||
deviceController.setSaveDataDir(dir)
|
deviceController.setSaveDataDir(dir)
|
||||||
file_browser.setCurrentDirectory(dir)
|
file_browser.setCurrentDirectory(dir)
|
||||||
root.finished()
|
root.finished()
|
||||||
|
|||||||
@@ -171,10 +171,6 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanFolderUrl(url) {
|
|
||||||
return decodeURIComponent(String(url).replace(/^file:\/\//, ""))
|
|
||||||
}
|
|
||||||
|
|
||||||
function _doDetect() {
|
function _doDetect() {
|
||||||
root.memoryRead = false
|
root.memoryRead = false
|
||||||
root.waferDetected = false
|
root.waferDetected = false
|
||||||
@@ -187,7 +183,7 @@ Rectangle {
|
|||||||
id: saveDirDialog
|
id: saveDirDialog
|
||||||
title: "Choose a folder to save Data"
|
title: "Choose a folder to save Data"
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
var dir = root.cleanFolderUrl(selectedFolder)
|
var dir = String(selectedFolder)
|
||||||
deviceController.setSaveDataDir(dir)
|
deviceController.setSaveDataDir(dir)
|
||||||
file_browser.setCurrentDirectory(dir)
|
file_browser.setCurrentDirectory(dir)
|
||||||
root._doDetect()
|
root._doDetect()
|
||||||
|
|||||||
@@ -113,9 +113,6 @@ ColumnLayout {
|
|||||||
readonly property bool isBusy: deviceController.connectionStatus.endsWith("...")
|
readonly property bool isBusy: deviceController.connectionStatus.endsWith("...")
|
||||||
readonly property string portName: deviceController.selectedPort || "—"
|
readonly property string portName: deviceController.selectedPort || "—"
|
||||||
|
|
||||||
function cleanFolderUrl(url) {
|
|
||||||
return decodeURIComponent(String(url).replace(/^file:\/\//, ""));
|
|
||||||
}
|
|
||||||
function currentFamilyCode() {
|
function currentFamilyCode() {
|
||||||
var info = deviceController.lastWaferInfo;
|
var info = deviceController.lastWaferInfo;
|
||||||
return info && info.length > 0 ? (info[0] || "") : "";
|
return info && info.length > 0 ? (info[0] || "") : "";
|
||||||
@@ -128,7 +125,7 @@ ColumnLayout {
|
|||||||
id: saveDirDialog
|
id: saveDirDialog
|
||||||
title: "Choose a folder to save."
|
title: "Choose a folder to save."
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
var dir = root.cleanFolderUrl(selectedFolder);
|
var dir = String(selectedFolder);
|
||||||
deviceController.setSaveDataDir(dir);
|
deviceController.setSaveDataDir(dir);
|
||||||
file_browser.setCurrentDirectory(dir);
|
file_browser.setCurrentDirectory(dir);
|
||||||
root.parseAndSavePendingRead();
|
root.parseAndSavePendingRead();
|
||||||
@@ -141,7 +138,7 @@ ColumnLayout {
|
|||||||
id: dirOnlyDialog
|
id: dirOnlyDialog
|
||||||
title: "Choose a folder to save Data"
|
title: "Choose a folder to save Data"
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
var dir = root.cleanFolderUrl(selectedFolder);
|
var dir = String(selectedFolder);
|
||||||
deviceController.setSaveDataDir(dir);
|
deviceController.setSaveDataDir(dir);
|
||||||
file_browser.setCurrentDirectory(dir);
|
file_browser.setCurrentDirectory(dir);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -260,14 +260,14 @@ ColumnLayout {
|
|||||||
id: importFileDialog
|
id: importFileDialog
|
||||||
title: "Select Data File To Import"
|
title: "Select Data File To Import"
|
||||||
nameFilters: ["CSV files (*.csv)"]
|
nameFilters: ["CSV files (*.csv)"]
|
||||||
onAccepted: file_browser.importZWaferCsv(String(selectedFile).replace(/^file:\/\//, ""))
|
onAccepted: file_browser.importZWaferCsv(String(selectedFile))
|
||||||
}
|
}
|
||||||
|
|
||||||
FolderDialog {
|
FolderDialog {
|
||||||
id: batchExportDirDialog
|
id: batchExportDirDialog
|
||||||
title: "Export Wafer Maps To…"
|
title: "Export Wafer Maps To…"
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
var dir = String(selectedFolder).replace(/^file:\/\//, "");
|
var dir = String(selectedFolder);
|
||||||
var paths = file_browser.files.map(function(row) { return row.fileName; });
|
var paths = file_browser.files.map(function(row) { return row.fileName; });
|
||||||
batchExportController.start(paths, dir);
|
batchExportController.start(paths, dir);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import threading
|
|||||||
|
|
||||||
from PySide6.QtCore import Property, QObject, Qt, Signal, Slot
|
from PySide6.QtCore import Property, QObject, Qt, Signal, Slot
|
||||||
|
|
||||||
from pygui.backend.utils import slot_error_boundary
|
from pygui.backend.utils import slot_error_boundary, to_local_path
|
||||||
from pygui.backend.wafer.batch_export import BatchExportResult, export_batch
|
from pygui.backend.wafer.batch_export import BatchExportResult, export_batch
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
@@ -33,7 +33,9 @@ class BatchExportController(QObject):
|
|||||||
self._busy = True
|
self._busy = True
|
||||||
self.busyChanged.emit()
|
self.busyChanged.emit()
|
||||||
threading.Thread(
|
threading.Thread(
|
||||||
target=self._worker, args=(list(file_paths), output_dir), daemon=True
|
target=self._worker,
|
||||||
|
args=(list(file_paths), str(to_local_path(output_dir))),
|
||||||
|
daemon=True,
|
||||||
).start()
|
).start()
|
||||||
|
|
||||||
def _worker(self, file_paths: list[str], output_dir: str) -> None:
|
def _worker(self, file_paths: list[str], output_dir: str) -> None:
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from PySide6.QtCore import Property, QObject, Qt, Signal, Slot
|
|||||||
|
|
||||||
from pygui.backend.data.local_settings import LocalSettings
|
from pygui.backend.data.local_settings import LocalSettings
|
||||||
from pygui.backend.models.data_model import TemperatureTableModel
|
from pygui.backend.models.data_model import TemperatureTableModel
|
||||||
from pygui.backend.utils import slot_error_boundary
|
from pygui.backend.utils import slot_error_boundary, to_local_path
|
||||||
from pygui.backend.visualization.graph_view import GraphView
|
from pygui.backend.visualization.graph_view import GraphView
|
||||||
from pygui.serialcomm.data_parser import (
|
from pygui.serialcomm.data_parser import (
|
||||||
convert_to_debug_temperatures,
|
convert_to_debug_temperatures,
|
||||||
@@ -82,7 +82,8 @@ class DeviceController(QObject):
|
|||||||
self._operation_in_progress = False
|
self._operation_in_progress = False
|
||||||
self._activity_log: list[str] = []
|
self._activity_log: list[str] = []
|
||||||
self._raw_bytes: Optional[bytes] = None
|
self._raw_bytes: Optional[bytes] = None
|
||||||
self._save_data_dir: str = getattr(settings, 'save_data_dir', "") or str(Path(self._data_dir) / "csv")
|
saved_dir = getattr(settings, 'save_data_dir', "")
|
||||||
|
self._save_data_dir: str = str(to_local_path(saved_dir)) if saved_dir else str(Path(self._data_dir) / "csv")
|
||||||
self._last_wafer_info: dict[str, Any] = {}
|
self._last_wafer_info: dict[str, Any] = {}
|
||||||
self._wafer_detected: bool = False
|
self._wafer_detected: bool = False
|
||||||
self._last_csv_path: str = ""
|
self._last_csv_path: str = ""
|
||||||
@@ -141,8 +142,8 @@ class DeviceController(QObject):
|
|||||||
@slot_error_boundary
|
@slot_error_boundary
|
||||||
def setSaveDataDir(self, path: str) -> None:
|
def setSaveDataDir(self, path: str) -> None:
|
||||||
"""Set the directory for saving parsed CSV data files."""
|
"""Set the directory for saving parsed CSV data files."""
|
||||||
self._save_data_dir = path
|
self._save_data_dir = str(to_local_path(path))
|
||||||
self._append_log(f"Save data dir set to: {path}")
|
self._append_log(f"Save data dir set to: {self._save_data_dir}")
|
||||||
self._save_status()
|
self._save_status()
|
||||||
|
|
||||||
@Slot(result=str)
|
@Slot(result=str)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from PySide6.QtWidgets import QFileDialog, QMessageBox
|
|||||||
|
|
||||||
from pygui.backend.data.constants import default_data_dir
|
from pygui.backend.data.constants import default_data_dir
|
||||||
from pygui.backend.data.csv_file_metadata import CSVFileMetadata
|
from pygui.backend.data.csv_file_metadata import CSVFileMetadata
|
||||||
|
from pygui.backend.utils import to_local_path
|
||||||
from pygui.backend.wafer.zwafer_parser import ZWaferParser
|
from pygui.backend.wafer.zwafer_parser import ZWaferParser
|
||||||
|
|
||||||
|
|
||||||
@@ -45,7 +46,7 @@ class FileBrowser(QObject):
|
|||||||
Used to keep this directory in sync with DeviceController.saveDataDir
|
Used to keep this directory in sync with DeviceController.saveDataDir
|
||||||
when the user picks a save folder elsewhere in the app.
|
when the user picks a save folder elsewhere in the app.
|
||||||
"""
|
"""
|
||||||
self._set_current_directory(Path(path))
|
self._set_current_directory(to_local_path(path))
|
||||||
self._refresh_files(show_empty_message=False)
|
self._refresh_files(show_empty_message=False)
|
||||||
|
|
||||||
@Slot()
|
@Slot()
|
||||||
@@ -124,7 +125,8 @@ class FileBrowser(QObject):
|
|||||||
date still gets a deterministic (if degenerate) filename, so the
|
date still gets a deterministic (if degenerate) filename, so the
|
||||||
duplicate-import guard below still fires on a second attempt.
|
duplicate-import guard below still fires on a second attempt.
|
||||||
"""
|
"""
|
||||||
source_path = Path(file_path)
|
source_path = to_local_path(file_path)
|
||||||
|
file_path = str(source_path)
|
||||||
parser_data, _ = self._parser.parse(file_path)
|
parser_data, _ = self._parser.parse(file_path)
|
||||||
if parser_data is None:
|
if parser_data is None:
|
||||||
self.importFinished.emit("", f"Failed to parse CSV:\n{source_path.name}")
|
self.importFinished.emit("", f"Failed to parse CSV:\n{source_path.name}")
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import shutil
|
|||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from PySide6.QtCore import Property, QObject, QUrl, Signal, Slot
|
from PySide6.QtCore import Property, QObject, Signal, Slot
|
||||||
|
|
||||||
from pygui.backend.license.license_manager import (
|
from pygui.backend.license.license_manager import (
|
||||||
LICENSE_FILENAME_RE,
|
LICENSE_FILENAME_RE,
|
||||||
@@ -13,25 +13,13 @@ from pygui.backend.license.license_manager import (
|
|||||||
parse_license_blob,
|
parse_license_blob,
|
||||||
read_license_files,
|
read_license_files,
|
||||||
)
|
)
|
||||||
|
from pygui.backend.utils import to_local_path as _to_local_path
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
TRIAL_DAYS = 30
|
TRIAL_DAYS = 30
|
||||||
|
|
||||||
|
|
||||||
def _to_local_path(path_or_url: str) -> Path:
|
|
||||||
"""Accept both plain paths and file:// URLs (QML FileDialog gives URLs).
|
|
||||||
|
|
||||||
Uses QUrl.toLocalFile() rather than urlparse — a manual
|
|
||||||
urlparse().path strip leaves a leading slash before a Windows drive
|
|
||||||
letter (e.g. "/C:/Users/...", not a valid absolute path), which
|
|
||||||
QUrl's platform-aware conversion handles correctly.
|
|
||||||
"""
|
|
||||||
if path_or_url.startswith("file:"):
|
|
||||||
return Path(QUrl(path_or_url).toLocalFile())
|
|
||||||
return Path(path_or_url)
|
|
||||||
|
|
||||||
|
|
||||||
# ===== License Model =====
|
# ===== License Model =====
|
||||||
class LicenseModel(QObject):
|
class LicenseModel(QObject):
|
||||||
"""Context property backing the About dialog license grid.
|
"""Context property backing the About dialog license grid.
|
||||||
|
|||||||
@@ -1,5 +1,26 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from PySide6.QtCore import QUrl
|
||||||
|
|
||||||
|
_MANGLED_DRIVE_RE = re.compile(r"^/([A-Za-z]:)")
|
||||||
|
|
||||||
|
|
||||||
|
def to_local_path(path_or_url: str) -> Path:
|
||||||
|
"""Accept plain paths, file:// URLs, and already-mangled "/C:/..." strings
|
||||||
|
(persisted by old buggy code before it stripped file:// by hand).
|
||||||
|
|
||||||
|
Uses QUrl.toLocalFile() rather than a manual string strip — stripping
|
||||||
|
"file://" by hand leaves a leading slash before a Windows drive letter
|
||||||
|
(e.g. "/C:/Users/...", not a valid absolute path), which QUrl's
|
||||||
|
platform-aware conversion handles correctly.
|
||||||
|
"""
|
||||||
|
if path_or_url.startswith("file:"):
|
||||||
|
return Path(QUrl(path_or_url).toLocalFile())
|
||||||
|
# ponytail: repair values saved by the old regex-stripping QML code
|
||||||
|
return Path(_MANGLED_DRIVE_RE.sub(r"\1", path_or_url))
|
||||||
|
|
||||||
|
|
||||||
def slot_error_boundary(func):
|
def slot_error_boundary(func):
|
||||||
|
|||||||
Reference in New Issue
Block a user