diff --git a/qtapputils/icons.py b/qtapputils/icons.py index c2a0b78..2578772 100644 --- a/qtapputils/icons.py +++ b/qtapputils/icons.py @@ -50,6 +50,12 @@ def get_standard_iconsize(constant: 'str') -> int: return style.pixelMetric(QStyle.PM_MessageBoxIconSize) elif constant == 'small': return style.pixelMetric(QStyle.PM_SmallIconSize) + elif constant == 'large': + return style.pixelMetric(QStyle.PM_LargeIconSize) + elif constant == 'toolbar': + return style.pixelMetric(QStyle.PM_ToolBarIconSize) + elif constant == 'button': + return style.pixelMetric(QStyle.PM_ButtonIconSize) class IconManager: diff --git a/qtapputils/widgets/path.py b/qtapputils/widgets/path.py index 83dbc0f..f34cdce 100644 --- a/qtapputils/widgets/path.py +++ b/qtapputils/widgets/path.py @@ -15,20 +15,49 @@ # ---- Third party imports from qtpy.QtCore import Signal +from qtpy.QtGui import QIcon from qtpy.QtWidgets import ( QCheckBox, QFrame, QLineEdit, QLabel, QFileDialog, QPushButton, QGridLayout, QWidget) +# ---- Local imports +from qtapputils.qthelpers import create_toolbutton + class PathBoxWidget(QFrame): """ - A widget to display and select a directory or file location. + a widget to display and select a directory or file location. + + Features + -------- + - Read-only line edit showing the current path. + - Browse button to open a QFileDialog for selecting a path. + - Optionally, uses a custom icon for the browse button. + - Emits `sig_path_changed` when the path changes. + + Parameters + ---------- + parent : QWidget, optional + Parent widget. + path_type : str, optional + Type of path dialog: 'getExistingDirectory', 'getOpenFileName', or + 'getSaveFileName'. + filters : str, optional + Filter string for file dialogs. + gettext : Callable, optional + Translation function for GUI strings. + browse_icon : QIcon, optional + Custom icon for the browse button. """ sig_path_changed = Signal(str) - def __init__(self, parent: QWidget = None, path: str = '', - directory: str = '', path_type: str = 'getExistingDirectory', - filters: str = None, gettext: Callable = None): + def __init__( + self, + parent: QWidget = None, + path_type: str = 'getExistingDirectory', + filters: str = None, + gettext: Callable = None, + browse_icon: QIcon = None): super().__init__(parent) _ = gettext if gettext else lambda x: x @@ -39,21 +68,27 @@ def __init__(self, parent: QWidget = None, path: str = '', elif path_type == 'getSaveFileName': self._caption = _('Save File') - self._directory = directory + self._directory = osp.expanduser('~') self.filters = filters self._path_type = path_type - self.browse_btn = QPushButton(_("Browse...")) - self.browse_btn.setDefault(False) - self.browse_btn.setAutoDefault(False) - self.browse_btn.clicked.connect(self.browse_path) - self.path_lineedit = QLineEdit() self.path_lineedit.setReadOnly(True) - self.path_lineedit.setText(path) - self.path_lineedit.setToolTip(path) - self.path_lineedit.setFixedHeight( - self.browse_btn.sizeHint().height() - 2) + if browse_icon is None: + self.browse_btn = QPushButton(_("Browse...")) + self.browse_btn.setDefault(False) + self.browse_btn.setAutoDefault(False) + self.browse_btn.clicked.connect(self.browse_path) + # Align line edit height with button. + self.path_lineedit.setFixedHeight( + self.browse_btn.sizeHint().height() - 2) + else: + self.browse_btn = create_toolbutton( + self, + icon=browse_icon, + text=_("Browse..."), + triggered=self.browse_path, + ) layout = QGridLayout(self) layout.setContentsMargins(0, 0, 0, 0) @@ -61,21 +96,28 @@ def __init__(self, parent: QWidget = None, path: str = '', layout.addWidget(self.path_lineedit, 0, 0) layout.addWidget(self.browse_btn, 0, 1) - def is_valid(self): - """Return whether path is valid.""" + def is_valid(self) -> bool: + """Return True if the current path exists on disk.""" return osp.exists(self.path()) - def is_empty(self): - """Return whether the path is empty.""" - return self.path_lineedit.text() == '' + def is_empty(self) -> bool: + """Return True if the current path is empty.""" + return not self.path_lineedit.text().strip() - def path(self): - """Return the path of this pathbox widget.""" + def path(self) -> str: + """Return the currently displayed path.""" return self.path_lineedit.text() def set_path(self, path: str): - """Set the path to the specified value.""" - if path == self.path: + """ + Set the path to the specified value. + + Parameters + ---------- + path : str + The new path to display and set as default directory. + """ + if path == self.path(): return self.path_lineedit.setText(path) @@ -90,22 +132,29 @@ def browse_path(self): self, self._caption, self.directory(), options=QFileDialog.ShowDirsOnly) elif self._path_type == 'getOpenFileName': - path, ext = QFileDialog.getOpenFileName( + path, _ = QFileDialog.getOpenFileName( self, self._caption, self.directory(), self.filters) elif self._path_type == 'getSaveFileName': - path, ext = QFileDialog.getSaveFileName( + path, _ = QFileDialog.getSaveFileName( self, self._caption, self.directory(), self.filters) if path: self.set_path(path) - def directory(self): + def directory(self) -> str: """Return the directory that is used by the QFileDialog.""" return (self._directory if osp.exists(self._directory) else osp.expanduser('~')) def set_directory(self, directory: str = path): - """Set the default directory that will be used by the QFileDialog.""" + """ + Set the default directory for file dialogs. + + Parameters + ---------- + directory : str or None + Directory path to set as default. + """ if directory is not None and osp.exists(directory): self._directory = directory diff --git a/qtapputils/widgets/tests/test_path.py b/qtapputils/widgets/tests/test_path.py index d4287d3..ba0f0af 100644 --- a/qtapputils/widgets/tests/test_path.py +++ b/qtapputils/widgets/tests/test_path.py @@ -17,6 +17,7 @@ # ---- Third party imports import pytest from qtpy.QtCore import Qt +from qtpy.QtGui import QIcon # ---- Local imports from qtapputils.widgets.path import PathBoxWidget, QFileDialog @@ -29,8 +30,6 @@ def pathbox(qtbot): pathbox = PathBoxWidget( parent=None, - path='', - directory='', path_type='getSaveFileName', filters=None ) @@ -42,32 +41,138 @@ def pathbox(qtbot): # ============================================================================= # ---- Tests for the PathBoxWidget # ============================================================================= -def test_getopen_filename(qtbot, pathbox, mocker, tmp_path): - """Test that getting a file name is working as expected.""" +def test_initialization_no_icon(qtbot): + pathbox = PathBoxWidget() + qtbot.addWidget(pathbox) + + assert pathbox.is_empty() assert not pathbox.is_valid() + assert pathbox.path() == "" + assert pathbox.directory() + + +def test_initialization_with_icon(qtbot): + icon = QIcon() + pathbox = PathBoxWidget(browse_icon=icon) + qtbot.addWidget(pathbox) + assert pathbox.is_empty() - assert pathbox.path() == '' - assert osp.samefile(pathbox.directory(), osp.expanduser('~')) + assert not pathbox.is_valid() - # Create an empty file. - selectedfilter = 'Text File (*.txt)' - selectedfilename = osp.join(tmp_path, 'pathbox_testfile.txt') - with open(selectedfilename, 'w') as txtfile: - txtfile.write('test') - # Patch the open file dialog and select the test file. +def test_set_and_get_path_valid(tmp_path, qtbot): + # Create a valid file + f = tmp_path / "myfile.txt" + f.write_text("content") + + pathbox = PathBoxWidget(path_type="getOpenFileName") + qtbot.addWidget(pathbox) + pathbox.set_path(str(f)) + + assert not pathbox.is_empty() + assert pathbox.is_valid() + assert pathbox.path() == str(f) + assert pathbox.directory() == str(tmp_path) + + +def test_set_and_get_path_invalid(qtbot): + pathbox = PathBoxWidget() + qtbot.addWidget(pathbox) + pathbox.set_path("/tmp/nonexistent_file.txt") + + assert pathbox.path() == "/tmp/nonexistent_file.txt" + assert pathbox.directory() == osp.expanduser('~') + assert not pathbox.is_valid() + + +def test_set_directory_valid(tmp_path, qtbot): + d = tmp_path / "dir" + d.mkdir() + + pathbox = PathBoxWidget() + qtbot.addWidget(pathbox) + pathbox.set_directory(str(d)) + + assert pathbox.directory() == str(d) + + +def test_set_directory_invalid(qtbot): + pathbox = PathBoxWidget() + qtbot.addWidget(pathbox) + pathbox.set_directory("/tmp/nonexistent_dir") + + # Should fallback to home if directory is invalid + assert pathbox.directory() == osp.expanduser('~') + + +def test_signal_emitted_on_path_change(tmp_path, qtbot): + f = tmp_path / "file.txt" + f.write_text("abc") + + pathbox = PathBoxWidget() + qtbot.addWidget(pathbox) + + with qtbot.waitSignal(pathbox.sig_path_changed) as blocker: + pathbox.set_path(str(f)) + assert blocker.args == [str(f)] + + +def test_browse_path_get_existing_directory(mocker, tmp_path, qtbot): + d = tmp_path / "bro_dir" + d.mkdir() + + pathbox = PathBoxWidget(path_type="getExistingDirectory") + qtbot.addWidget(pathbox) + qfdialog_patcher = mocker.patch.object( QFileDialog, - 'getSaveFileName', - return_value=(selectedfilename, selectedfilter) + 'getExistingDirectory', + return_value=str(d) ) - qtbot.mouseClick(pathbox.browse_btn, Qt.LeftButton) + pathbox.browse_path() assert qfdialog_patcher.call_count == 1 + assert pathbox.path() == str(d) + assert pathbox.directory() == str(tmp_path) assert pathbox.is_valid() - assert not pathbox.is_empty() - assert pathbox.path() == selectedfilename - assert osp.samefile(pathbox.directory(), tmp_path) + + +def test_browse_path_get_open_file_name(mocker, tmp_path, qtbot): + f = tmp_path / "bro_file.txt" + f.write_text("abc") + + pathbox = PathBoxWidget(path_type="getOpenFileName") + qtbot.addWidget(pathbox) + + qfdialog_patcher = mocker.patch.object( + QFileDialog, + 'getOpenFileName', + return_value=(str(f), "") + ) + + pathbox.browse_path() + assert qfdialog_patcher.call_count == 1 + assert pathbox.path() == str(f) + assert pathbox.directory() == str(tmp_path) + assert pathbox.is_valid() + + +def test_browse_path_get_save_file_name(mocker, tmp_path, qtbot): + f = tmp_path / "save_file.txt" + + pathbox = PathBoxWidget(path_type="getSaveFileName") + qtbot.addWidget(pathbox) + + qfdialog_patcher = mocker.patch.object( + QFileDialog, + 'getSaveFileName', + return_value=(str(f), "") + ) + + pathbox.browse_path() + assert qfdialog_patcher.call_count == 1 + assert pathbox.path() == str(f) + assert pathbox.directory() == str(tmp_path) if __name__ == "__main__":