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
6 changes: 6 additions & 0 deletions qtapputils/icons.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
103 changes: 76 additions & 27 deletions qtapputils/widgets/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -39,43 +68,56 @@ 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)
layout.setSpacing(3)
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)
Expand All @@ -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

Expand Down
141 changes: 123 additions & 18 deletions qtapputils/widgets/tests/test_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -29,8 +30,6 @@
def pathbox(qtbot):
pathbox = PathBoxWidget(
parent=None,
path='',
directory='',
path_type='getSaveFileName',
filters=None
)
Expand All @@ -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__":
Expand Down