Skip to content

Commit a1f7e48

Browse files
Merge pull request #44 from geo-stack/add_option_pathbox_widget_to_use_browse_icon
PR: Improve PathBoxWidget and add option to use a QToolButton for 'Browse'
2 parents 50c94cf + 7fbca1b commit a1f7e48

File tree

3 files changed

+205
-45
lines changed

3 files changed

+205
-45
lines changed

qtapputils/icons.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ def get_standard_iconsize(constant: 'str') -> int:
5050
return style.pixelMetric(QStyle.PM_MessageBoxIconSize)
5151
elif constant == 'small':
5252
return style.pixelMetric(QStyle.PM_SmallIconSize)
53+
elif constant == 'large':
54+
return style.pixelMetric(QStyle.PM_LargeIconSize)
55+
elif constant == 'toolbar':
56+
return style.pixelMetric(QStyle.PM_ToolBarIconSize)
57+
elif constant == 'button':
58+
return style.pixelMetric(QStyle.PM_ButtonIconSize)
5359

5460

5561
class IconManager:

qtapputils/widgets/path.py

Lines changed: 76 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,49 @@
1515

1616
# ---- Third party imports
1717
from qtpy.QtCore import Signal
18+
from qtpy.QtGui import QIcon
1819
from qtpy.QtWidgets import (
1920
QCheckBox, QFrame, QLineEdit, QLabel, QFileDialog, QPushButton,
2021
QGridLayout, QWidget)
2122

23+
# ---- Local imports
24+
from qtapputils.qthelpers import create_toolbutton
25+
2226

2327
class PathBoxWidget(QFrame):
2428
"""
25-
A widget to display and select a directory or file location.
29+
a widget to display and select a directory or file location.
30+
31+
Features
32+
--------
33+
- Read-only line edit showing the current path.
34+
- Browse button to open a QFileDialog for selecting a path.
35+
- Optionally, uses a custom icon for the browse button.
36+
- Emits `sig_path_changed` when the path changes.
37+
38+
Parameters
39+
----------
40+
parent : QWidget, optional
41+
Parent widget.
42+
path_type : str, optional
43+
Type of path dialog: 'getExistingDirectory', 'getOpenFileName', or
44+
'getSaveFileName'.
45+
filters : str, optional
46+
Filter string for file dialogs.
47+
gettext : Callable, optional
48+
Translation function for GUI strings.
49+
browse_icon : QIcon, optional
50+
Custom icon for the browse button.
2651
"""
2752
sig_path_changed = Signal(str)
2853

29-
def __init__(self, parent: QWidget = None, path: str = '',
30-
directory: str = '', path_type: str = 'getExistingDirectory',
31-
filters: str = None, gettext: Callable = None):
54+
def __init__(
55+
self,
56+
parent: QWidget = None,
57+
path_type: str = 'getExistingDirectory',
58+
filters: str = None,
59+
gettext: Callable = None,
60+
browse_icon: QIcon = None):
3261
super().__init__(parent)
3362

3463
_ = gettext if gettext else lambda x: x
@@ -39,43 +68,56 @@ def __init__(self, parent: QWidget = None, path: str = '',
3968
elif path_type == 'getSaveFileName':
4069
self._caption = _('Save File')
4170

42-
self._directory = directory
71+
self._directory = osp.expanduser('~')
4372
self.filters = filters
4473
self._path_type = path_type
4574

46-
self.browse_btn = QPushButton(_("Browse..."))
47-
self.browse_btn.setDefault(False)
48-
self.browse_btn.setAutoDefault(False)
49-
self.browse_btn.clicked.connect(self.browse_path)
50-
5175
self.path_lineedit = QLineEdit()
5276
self.path_lineedit.setReadOnly(True)
53-
self.path_lineedit.setText(path)
54-
self.path_lineedit.setToolTip(path)
55-
self.path_lineedit.setFixedHeight(
56-
self.browse_btn.sizeHint().height() - 2)
77+
if browse_icon is None:
78+
self.browse_btn = QPushButton(_("Browse..."))
79+
self.browse_btn.setDefault(False)
80+
self.browse_btn.setAutoDefault(False)
81+
self.browse_btn.clicked.connect(self.browse_path)
82+
# Align line edit height with button.
83+
self.path_lineedit.setFixedHeight(
84+
self.browse_btn.sizeHint().height() - 2)
85+
else:
86+
self.browse_btn = create_toolbutton(
87+
self,
88+
icon=browse_icon,
89+
text=_("Browse..."),
90+
triggered=self.browse_path,
91+
)
5792

5893
layout = QGridLayout(self)
5994
layout.setContentsMargins(0, 0, 0, 0)
6095
layout.setSpacing(3)
6196
layout.addWidget(self.path_lineedit, 0, 0)
6297
layout.addWidget(self.browse_btn, 0, 1)
6398

64-
def is_valid(self):
65-
"""Return whether path is valid."""
99+
def is_valid(self) -> bool:
100+
"""Return True if the current path exists on disk."""
66101
return osp.exists(self.path())
67102

68-
def is_empty(self):
69-
"""Return whether the path is empty."""
70-
return self.path_lineedit.text() == ''
103+
def is_empty(self) -> bool:
104+
"""Return True if the current path is empty."""
105+
return not self.path_lineedit.text().strip()
71106

72-
def path(self):
73-
"""Return the path of this pathbox widget."""
107+
def path(self) -> str:
108+
"""Return the currently displayed path."""
74109
return self.path_lineedit.text()
75110

76111
def set_path(self, path: str):
77-
"""Set the path to the specified value."""
78-
if path == self.path:
112+
"""
113+
Set the path to the specified value.
114+
115+
Parameters
116+
----------
117+
path : str
118+
The new path to display and set as default directory.
119+
"""
120+
if path == self.path():
79121
return
80122

81123
self.path_lineedit.setText(path)
@@ -90,22 +132,29 @@ def browse_path(self):
90132
self, self._caption, self.directory(),
91133
options=QFileDialog.ShowDirsOnly)
92134
elif self._path_type == 'getOpenFileName':
93-
path, ext = QFileDialog.getOpenFileName(
135+
path, _ = QFileDialog.getOpenFileName(
94136
self, self._caption, self.directory(), self.filters)
95137
elif self._path_type == 'getSaveFileName':
96-
path, ext = QFileDialog.getSaveFileName(
138+
path, _ = QFileDialog.getSaveFileName(
97139
self, self._caption, self.directory(), self.filters)
98140

99141
if path:
100142
self.set_path(path)
101143

102-
def directory(self):
144+
def directory(self) -> str:
103145
"""Return the directory that is used by the QFileDialog."""
104146
return (self._directory if osp.exists(self._directory) else
105147
osp.expanduser('~'))
106148

107149
def set_directory(self, directory: str = path):
108-
"""Set the default directory that will be used by the QFileDialog."""
150+
"""
151+
Set the default directory for file dialogs.
152+
153+
Parameters
154+
----------
155+
directory : str or None
156+
Directory path to set as default.
157+
"""
109158
if directory is not None and osp.exists(directory):
110159
self._directory = directory
111160

qtapputils/widgets/tests/test_path.py

Lines changed: 123 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
# ---- Third party imports
1818
import pytest
1919
from qtpy.QtCore import Qt
20+
from qtpy.QtGui import QIcon
2021

2122
# ---- Local imports
2223
from qtapputils.widgets.path import PathBoxWidget, QFileDialog
@@ -29,8 +30,6 @@
2930
def pathbox(qtbot):
3031
pathbox = PathBoxWidget(
3132
parent=None,
32-
path='',
33-
directory='',
3433
path_type='getSaveFileName',
3534
filters=None
3635
)
@@ -42,32 +41,138 @@ def pathbox(qtbot):
4241
# =============================================================================
4342
# ---- Tests for the PathBoxWidget
4443
# =============================================================================
45-
def test_getopen_filename(qtbot, pathbox, mocker, tmp_path):
46-
"""Test that getting a file name is working as expected."""
44+
def test_initialization_no_icon(qtbot):
45+
pathbox = PathBoxWidget()
46+
qtbot.addWidget(pathbox)
47+
48+
assert pathbox.is_empty()
4749
assert not pathbox.is_valid()
50+
assert pathbox.path() == ""
51+
assert pathbox.directory()
52+
53+
54+
def test_initialization_with_icon(qtbot):
55+
icon = QIcon()
56+
pathbox = PathBoxWidget(browse_icon=icon)
57+
qtbot.addWidget(pathbox)
58+
4859
assert pathbox.is_empty()
49-
assert pathbox.path() == ''
50-
assert osp.samefile(pathbox.directory(), osp.expanduser('~'))
60+
assert not pathbox.is_valid()
5161

52-
# Create an empty file.
53-
selectedfilter = 'Text File (*.txt)'
54-
selectedfilename = osp.join(tmp_path, 'pathbox_testfile.txt')
55-
with open(selectedfilename, 'w') as txtfile:
56-
txtfile.write('test')
5762

58-
# Patch the open file dialog and select the test file.
63+
def test_set_and_get_path_valid(tmp_path, qtbot):
64+
# Create a valid file
65+
f = tmp_path / "myfile.txt"
66+
f.write_text("content")
67+
68+
pathbox = PathBoxWidget(path_type="getOpenFileName")
69+
qtbot.addWidget(pathbox)
70+
pathbox.set_path(str(f))
71+
72+
assert not pathbox.is_empty()
73+
assert pathbox.is_valid()
74+
assert pathbox.path() == str(f)
75+
assert pathbox.directory() == str(tmp_path)
76+
77+
78+
def test_set_and_get_path_invalid(qtbot):
79+
pathbox = PathBoxWidget()
80+
qtbot.addWidget(pathbox)
81+
pathbox.set_path("/tmp/nonexistent_file.txt")
82+
83+
assert pathbox.path() == "/tmp/nonexistent_file.txt"
84+
assert pathbox.directory() == osp.expanduser('~')
85+
assert not pathbox.is_valid()
86+
87+
88+
def test_set_directory_valid(tmp_path, qtbot):
89+
d = tmp_path / "dir"
90+
d.mkdir()
91+
92+
pathbox = PathBoxWidget()
93+
qtbot.addWidget(pathbox)
94+
pathbox.set_directory(str(d))
95+
96+
assert pathbox.directory() == str(d)
97+
98+
99+
def test_set_directory_invalid(qtbot):
100+
pathbox = PathBoxWidget()
101+
qtbot.addWidget(pathbox)
102+
pathbox.set_directory("/tmp/nonexistent_dir")
103+
104+
# Should fallback to home if directory is invalid
105+
assert pathbox.directory() == osp.expanduser('~')
106+
107+
108+
def test_signal_emitted_on_path_change(tmp_path, qtbot):
109+
f = tmp_path / "file.txt"
110+
f.write_text("abc")
111+
112+
pathbox = PathBoxWidget()
113+
qtbot.addWidget(pathbox)
114+
115+
with qtbot.waitSignal(pathbox.sig_path_changed) as blocker:
116+
pathbox.set_path(str(f))
117+
assert blocker.args == [str(f)]
118+
119+
120+
def test_browse_path_get_existing_directory(mocker, tmp_path, qtbot):
121+
d = tmp_path / "bro_dir"
122+
d.mkdir()
123+
124+
pathbox = PathBoxWidget(path_type="getExistingDirectory")
125+
qtbot.addWidget(pathbox)
126+
59127
qfdialog_patcher = mocker.patch.object(
60128
QFileDialog,
61-
'getSaveFileName',
62-
return_value=(selectedfilename, selectedfilter)
129+
'getExistingDirectory',
130+
return_value=str(d)
63131
)
64-
qtbot.mouseClick(pathbox.browse_btn, Qt.LeftButton)
65132

133+
pathbox.browse_path()
66134
assert qfdialog_patcher.call_count == 1
135+
assert pathbox.path() == str(d)
136+
assert pathbox.directory() == str(tmp_path)
67137
assert pathbox.is_valid()
68-
assert not pathbox.is_empty()
69-
assert pathbox.path() == selectedfilename
70-
assert osp.samefile(pathbox.directory(), tmp_path)
138+
139+
140+
def test_browse_path_get_open_file_name(mocker, tmp_path, qtbot):
141+
f = tmp_path / "bro_file.txt"
142+
f.write_text("abc")
143+
144+
pathbox = PathBoxWidget(path_type="getOpenFileName")
145+
qtbot.addWidget(pathbox)
146+
147+
qfdialog_patcher = mocker.patch.object(
148+
QFileDialog,
149+
'getOpenFileName',
150+
return_value=(str(f), "")
151+
)
152+
153+
pathbox.browse_path()
154+
assert qfdialog_patcher.call_count == 1
155+
assert pathbox.path() == str(f)
156+
assert pathbox.directory() == str(tmp_path)
157+
assert pathbox.is_valid()
158+
159+
160+
def test_browse_path_get_save_file_name(mocker, tmp_path, qtbot):
161+
f = tmp_path / "save_file.txt"
162+
163+
pathbox = PathBoxWidget(path_type="getSaveFileName")
164+
qtbot.addWidget(pathbox)
165+
166+
qfdialog_patcher = mocker.patch.object(
167+
QFileDialog,
168+
'getSaveFileName',
169+
return_value=(str(f), "")
170+
)
171+
172+
pathbox.browse_path()
173+
assert qfdialog_patcher.call_count == 1
174+
assert pathbox.path() == str(f)
175+
assert pathbox.directory() == str(tmp_path)
71176

72177

73178
if __name__ == "__main__":

0 commit comments

Comments
 (0)