Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.

### Fixed
- Pressing Delete or F2 while editing a text field no longer triggers file delete or rename — those keys now act only inside the file panes, and file operations tell you to focus a pane first instead of guessing (and sometimes targeting a remote file you never touched).
- WebDAV connections now force an empty library root so opening subfolders keeps valid URLs instead of accidentally generating malformed nested paths.
- Background transfer completions no longer jump your position in the file lists back to the top; deletes and renames keep you on the nearest neighbouring file, and sorting or toggling hidden files keeps your selection.
- The "Announce file count" setting now actually speaks the count when entering a directory; empty folders and empty filter results are announced instead of silent.
- The Speech settings tab (rate, volume, verbosity) now controls the built-in speech output instead of doing nothing; verbosity also controls transfer progress announcements.
Expand Down
31 changes: 16 additions & 15 deletions src/portkeydrop/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,14 +184,6 @@ def __init__(self) -> None:
self._exit_sound_played = False
self._announcer = ScreenReaderAnnouncer()
self._apply_speech_settings()
if not self._announcer.is_available():
# Deferred so the activity log exists when this runs.
wx.CallAfter(
self.log_event,
"Speech output is unavailable; announcements will appear in the "
"status bar and this log only. Install the 'prismatoid' package "
"to enable speech.",
)
self._soundpacks_dir = ensure_default_soundpack(get_soundpacks_dir())
audio_settings = getattr(self._settings, "audio", None)
self._sound_player = SoundPlayer(
Expand All @@ -208,6 +200,13 @@ def __init__(self) -> None:
self._build_toolbar()
self._build_dual_pane()
self._build_status_bar()
if not self._announcer.is_available():
wx.CallAfter(
self.log_event,
"Speech output is unavailable; announcements will appear in the "
"status bar and this log only. Install the 'prismatoid' package "
"to enable speech.",
)
self._bind_events()
self._update_title()
self._refresh_local_files()
Expand Down Expand Up @@ -2662,13 +2661,15 @@ def log_event(self, message: str) -> None:
"""Append a timestamped entry to the activity log and announce it."""
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
entry = f"[{timestamp}] {message}\n"
if self.FindFocus() is self.activity_log:
# Don't yank the review cursor while the user is reading the log.
position = self.activity_log.GetInsertionPoint()
self.activity_log.AppendText(entry)
self.activity_log.SetInsertionPoint(position)
else:
self.activity_log.AppendText(entry)
activity_log = getattr(self, "activity_log", None)
if activity_log is not None:
if self.FindFocus() is activity_log:
# Don't yank the review cursor while the user is reading the log.
position = activity_log.GetInsertionPoint()
activity_log.AppendText(entry)
activity_log.SetInsertionPoint(position)
else:
activity_log.AppendText(entry)
self._announce(message)

def _announce(self, message: str) -> None:
Expand Down
1 change: 1 addition & 0 deletions src/portkeydrop/protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,7 @@ def connect(self) -> None:
self._client = Client(
{
"webdav_hostname": self._build_hostname(),
"webdav_root": "",
"webdav_login": self._info.username,
"webdav_password": self._info.password,
"webdav_timeout": self._info.timeout,
Expand Down
13 changes: 13 additions & 0 deletions tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,19 @@ def test_main_frame_init_sets_transfer_state(tmp_path, app_module):
transfer_service_cls.assert_called_once_with(notify_window=frame, max_workers=2)


def test_log_event_preserves_review_cursor_when_activity_log_has_focus(app_module):
app, _ = app_module
frame = _hydrate_frame(app_module)
frame.FindFocus = MagicMock(return_value=frame.activity_log)
frame.activity_log.GetInsertionPoint.return_value = 7

frame.log_event("Speech output is unavailable")

frame.activity_log.AppendText.assert_called_once()
frame.activity_log.SetInsertionPoint.assert_called_once_with(7)
frame._announce.assert_called_once_with("Speech output is unavailable")


def test_bind_events_hooks_transfer_update(app_module):
app, _ = app_module
frame = object.__new__(app.MainFrame)
Expand Down
2 changes: 2 additions & 0 deletions tests/test_protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ def test_connect_builds_webdavclient_options(self, monkeypatch):
client_class.assert_called_once_with(
{
"webdav_hostname": "https://dav.example.com",
"webdav_root": "",
"webdav_login": "alice",
"webdav_password": "secret",
"webdav_timeout": 12,
Expand Down Expand Up @@ -318,6 +319,7 @@ def test_connect_preserves_explicit_webdav_url_and_port(self, monkeypatch):
assert (
client_class.call_args.args[0]["webdav_hostname"] == "http://dav.example.com:8080/root"
)
assert client_class.call_args.args[0]["webdav_root"] == ""

def test_list_dir_maps_webdav_items(self, monkeypatch):
webdav = MagicMock()
Expand Down
Loading