Skip to content

Commit a284bd5

Browse files
committed
Added human readable file sizes, fixed some missing keyboard shortcuts.
1 parent 0e58a72 commit a284bd5

File tree

4 files changed

+169
-100
lines changed

4 files changed

+169
-100
lines changed

dialogs.py

Lines changed: 91 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# --- START OF FILE dialogs.py ---
2+
13
import wx
24
import theme
35
import stat
@@ -7,6 +9,21 @@
79
import shutil
810
from sftp_helpers import upload_item, download_item, delete_item
911

12+
def human_readable_size(size_bytes):
13+
"""Converts a size in bytes to a human-readable format."""
14+
if size_bytes < 1024:
15+
return f"{size_bytes} B"
16+
size_name = ("B", "KB", "MB", "GB", "TB")
17+
import math
18+
try:
19+
i = int(math.floor(math.log(size_bytes, 1024)))
20+
p = math.pow(1024, i)
21+
s = round(size_bytes / p, 1)
22+
return f"{s} {size_name[i]}"
23+
except (ValueError, IndexError):
24+
return str(size_bytes)
25+
26+
1027
class AddServerDialog(wx.Dialog):
1128
def __init__(self, parent, title="Add SSH Server", server_to_edit=None):
1229
super(AddServerDialog, self).__init__(parent, title=title, size=(400, 420))
@@ -140,12 +157,9 @@ class FileBrowserDialog(wx.Dialog):
140157
def __init__(self, parent, sftp_client, edit_callback):
141158
super(FileBrowserDialog, self).__init__(parent, title="SFTP File Browser", size=(600, 480))
142159

143-
self.sftp = sftp_client
144-
self.current_path = self.sftp.getcwd() or "/"
145-
self.edit_callback = edit_callback
146-
self.progress_dialog = None
147-
self.copy_temp_dir = None
148-
160+
self.sftp, self.current_path, self.edit_callback = sftp_client, sftp_client.getcwd() or "/", edit_callback
161+
self.progress_dialog, self.copy_temp_dir = None, None
162+
149163
panel = wx.Panel(self)
150164
self.vbox = wx.BoxSizer(wx.VERTICAL)
151165
self.path_text = wx.StaticText(panel, label=self.current_path)
@@ -157,14 +171,16 @@ def __init__(self, parent, sftp_client, edit_callback):
157171
self.vbox.Add(self.file_list, 1, wx.ALL|wx.EXPAND, 5)
158172

159173
hbox_buttons = wx.BoxSizer(wx.HORIZONTAL)
160-
self.upload_button = wx.Button(panel, label="Upload")
161-
self.download_button = wx.Button(panel, label="Download")
162-
self.copy_button = wx.Button(panel, label="Copy")
163-
self.edit_button = wx.Button(panel, label="Edit")
164-
self.delete_button = wx.Button(panel, label="Delete")
165-
close_button = wx.Button(panel, label="Close", id=wx.ID_CANCEL)
174+
self.upload_button = wx.Button(panel, label="&Upload")
175+
self.new_button = wx.Button(panel, label="&New...")
176+
self.download_button = wx.Button(panel, label="&Download")
177+
self.copy_button = wx.Button(panel, label="&Copy")
178+
self.edit_button = wx.Button(panel, label="&Edit")
179+
self.delete_button = wx.Button(panel, label="&Delete")
180+
close_button = wx.Button(panel, label="&Close", id=wx.ID_CANCEL)
166181

167182
hbox_buttons.Add(self.upload_button)
183+
hbox_buttons.Add(self.new_button, flag=wx.LEFT, border=5)
168184
hbox_buttons.Add(self.download_button, flag=wx.LEFT, border=5)
169185
hbox_buttons.Add(self.copy_button, flag=wx.LEFT, border=5)
170186
hbox_buttons.Add(self.edit_button, flag=wx.LEFT, border=5)
@@ -177,12 +193,14 @@ def __init__(self, parent, sftp_client, edit_callback):
177193
self.vbox.Add(self.status_text, 0, wx.LEFT|wx.RIGHT|wx.BOTTOM|wx.EXPAND, 5)
178194
panel.SetSizer(self.vbox)
179195

196+
self.Bind(wx.EVT_ACTIVATE, self.on_activate)
180197
self.file_list.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.on_item_activated)
181198
self.file_list.Bind(wx.EVT_LIST_ITEM_SELECTED, self.on_selection_changed)
182199
self.file_list.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.on_selection_changed)
183200
self.file_list.Bind(wx.EVT_KEY_DOWN, self.on_key_down)
184201
self.edit_button.Bind(wx.EVT_BUTTON, self.on_edit_button)
185202
self.upload_button.Bind(wx.EVT_BUTTON, self.on_upload)
203+
self.new_button.Bind(wx.EVT_BUTTON, self.on_new)
186204
self.download_button.Bind(wx.EVT_BUTTON, self.on_download)
187205
self.copy_button.Bind(wx.EVT_BUTTON, self.on_copy)
188206
self.delete_button.Bind(wx.EVT_BUTTON, self.on_delete)
@@ -191,6 +209,30 @@ def __init__(self, parent, sftp_client, edit_callback):
191209
theme.apply_dark_theme(self)
192210
self.populate_files()
193211

212+
def on_activate(self, event):
213+
if event.GetActive():
214+
self.populate_files()
215+
event.Skip()
216+
217+
def populate_files(self):
218+
self.path_text.SetLabel(f"Path: {self.current_path}")
219+
self.file_list.DeleteAllItems()
220+
if self.current_path != "/":
221+
index = self.file_list.InsertItem(self.file_list.GetItemCount(), "..")
222+
self.file_list.SetItem(index, 2, "Parent Directory")
223+
try:
224+
for attr in self.sftp.listdir_attr(self.current_path):
225+
index = self.file_list.InsertItem(self.file_list.GetItemCount(), attr.filename)
226+
self.file_list.SetItem(index, 1, human_readable_size(attr.st_size))
227+
file_mode = attr.st_mode
228+
if stat.S_ISDIR(file_mode):
229+
self.file_list.SetItem(index, 2, "Directory")
230+
else:
231+
self.file_list.SetItem(index, 2, "File")
232+
except Exception as e:
233+
wx.MessageBox(f"Could not list directory: {e}", "SFTP Error", wx.ICON_ERROR)
234+
self.on_selection_changed(None)
235+
194236
def on_close(self, event):
195237
if self.copy_temp_dir and os.path.exists(self.copy_temp_dir):
196238
try: shutil.rmtree(self.copy_temp_dir)
@@ -275,7 +317,7 @@ def _upload_worker(self, local_paths):
275317
try:
276318
for i, local_path in enumerate(local_paths):
277319
filename = os.path.basename(local_path)
278-
remote_path = f"{self.current_path}/{filename}"
320+
remote_path = f"{self.current_path}/{filename}" if self.current_path != "/" else f"/{filename}"
279321
wx.CallAfter(self.progress_dialog.Update, 0, f"Uploading {filename} ({i+1}/{len(local_paths)})...")
280322
def progress_callback(transferred, total):
281323
if self.progress_dialog:
@@ -363,23 +405,6 @@ def get_selected_remote_paths(self):
363405
else: paths.append(f"{self.current_path}/{name}")
364406
return paths
365407

366-
def populate_files(self):
367-
self.path_text.SetLabel(f"Path: {self.current_path}")
368-
self.file_list.DeleteAllItems()
369-
if self.current_path != "/":
370-
index = self.file_list.InsertItem(self.file_list.GetItemCount(), "..")
371-
self.file_list.SetItem(index, 2, "Parent Directory")
372-
try:
373-
for attr in self.sftp.listdir_attr(self.current_path):
374-
index = self.file_list.InsertItem(self.file_list.GetItemCount(), attr.filename)
375-
self.file_list.SetItem(index, 1, str(attr.st_size))
376-
file_mode = attr.st_mode
377-
if stat.S_ISDIR(file_mode): self.file_list.SetItem(index, 2, "Directory")
378-
else: self.file_list.SetItem(index, 2, "File")
379-
except Exception as e:
380-
wx.MessageBox(f"Could not list directory: {e}", "SFTP Error", wx.ICON_ERROR)
381-
self.on_selection_changed(None)
382-
383408
def on_item_activated(self, event):
384409
item_text = event.GetText()
385410
item_type = self.file_list.GetItemText(event.GetIndex(), col=2)
@@ -394,11 +419,44 @@ def on_edit_button(self, event):
394419
remote_paths = self.get_selected_remote_paths()
395420
if remote_paths:
396421
self.edit_callback(remote_paths[0])
397-
# self.Close() <-- This line is removed to keep the browser open.
422+
# self.Close() # This is correctly removed now.
398423

399-
def on_paste_upload(self):
424+
def on_paste_upload(self, event):
400425
data = wx.FileDataObject()
401426
if wx.TheClipboard.Open():
402427
success = wx.TheClipboard.GetData(data)
403428
wx.TheClipboard.Close()
404-
if success: self.start_upload_thread(data.GetFilenames())
429+
if success: self.start_upload_thread(data.GetFilenames())
430+
431+
def on_new(self, event):
432+
menu = wx.Menu()
433+
dir_item = menu.Append(wx.ID_ANY, "Directory")
434+
file_item = menu.Append(wx.ID_ANY, "File")
435+
self.Bind(wx.EVT_MENU, self.on_new_directory, dir_item)
436+
self.Bind(wx.EVT_MENU, self.on_new_file, file_item)
437+
self.PopupMenu(menu)
438+
439+
def on_new_directory(self, event):
440+
with wx.TextEntryDialog(self, "Enter name for the new directory:", "Create Directory") as dlg:
441+
if dlg.ShowModal() == wx.ID_OK:
442+
name = dlg.GetValue().strip()
443+
if not name: return
444+
remote_path = f"{self.current_path}/{name}" if self.current_path != "/" else f"/{name}"
445+
try:
446+
self.sftp.mkdir(remote_path)
447+
self.populate_files()
448+
except Exception as e:
449+
wx.MessageBox(f"Failed to create directory: {e}", "Error", wx.ICON_ERROR)
450+
451+
def on_new_file(self, event):
452+
with wx.TextEntryDialog(self, "Enter name for the new file:", "Create File") as dlg:
453+
if dlg.ShowModal() == wx.ID_OK:
454+
name = dlg.GetValue().strip()
455+
if not name: return
456+
remote_path = f"{self.current_path}/{name}" if self.current_path != "/" else f"/{name}"
457+
try:
458+
with self.sftp.open(remote_path, 'w') as f:
459+
pass
460+
self.populate_files()
461+
except Exception as e:
462+
wx.MessageBox(f"Failed to create file: {e}", "Error", wx.ICON_ERROR)

editor_frame.py

Lines changed: 63 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,42 @@
1+
# --- START OF FILE editor_frame.py ---
2+
13
import wx
24
import os
35
import theme
46
from menu_mixin import SettingsMenuMixin
57

6-
# --- NEW: Dialogs for Find and Replace functionality ---
78
class FindDialog(wx.Dialog):
89
def __init__(self, parent, title):
910
super(FindDialog, self).__init__(parent, title=title)
1011
self.parent = parent
1112
panel = wx.Panel(self)
1213
vbox = wx.BoxSizer(wx.VERTICAL)
13-
14-
self.find_text = wx.TextCtrl(panel)
14+
self.find_text = wx.TextCtrl(panel, style=wx.TE_PROCESS_ENTER)
1515
vbox.Add(wx.StaticText(panel, label="Find what:"), 0, wx.ALL, 5)
1616
vbox.Add(self.find_text, 0, wx.EXPAND|wx.ALL, 5)
1717

18+
# --- NEW: Direction controls ---
19+
self.direction_radio = wx.RadioBox(panel, label="Direction", choices=["Down", "Up"])
20+
vbox.Add(self.direction_radio, 0, wx.EXPAND|wx.ALL, 5)
21+
1822
self.case_checkbox = wx.CheckBox(panel, label="Match case")
1923
vbox.Add(self.case_checkbox, 0, wx.ALL, 5)
2024

21-
# --- FIX: Add a horizontal sizer for buttons ---
2225
hbox = wx.BoxSizer(wx.HORIZONTAL)
23-
find_next_btn = wx.Button(panel, label="Find Next")
24-
close_btn = wx.Button(panel, label="Close", id=wx.ID_CANCEL) # Use standard Cancel ID
26+
find_next_btn = wx.Button(panel, label="Find &Next")
27+
close_btn = wx.Button(panel, label="&Close", id=wx.ID_CANCEL)
2528
hbox.Add(find_next_btn)
2629
hbox.Add(close_btn, 0, wx.LEFT, 5)
2730
vbox.Add(hbox, 0, wx.ALIGN_CENTER|wx.ALL, 5)
2831

2932
panel.SetSizer(vbox)
3033
find_next_btn.Bind(wx.EVT_BUTTON, self.on_find_next)
34+
self.find_text.Bind(wx.EVT_TEXT_ENTER, self.on_find_next)
3135
self.find_text.SetFocus()
3236

3337
def on_find_next(self, event):
34-
self.parent.do_find(self.find_text.GetValue(), self.case_checkbox.IsChecked())
38+
direction = self.direction_radio.GetStringSelection().lower()
39+
self.parent.do_find(self.find_text.GetValue(), self.case_checkbox.IsChecked(), direction)
3540

3641
class ReplaceDialog(wx.Dialog):
3742
def __init__(self, parent, title):
@@ -41,44 +46,52 @@ def __init__(self, parent, title):
4146
vbox = wx.BoxSizer(wx.VERTICAL)
4247

4348
vbox.Add(wx.StaticText(panel, label="Find what:"), 0, wx.ALL, 5)
44-
self.find_text = wx.TextCtrl(panel)
49+
self.find_text = wx.TextCtrl(panel, style=wx.TE_PROCESS_ENTER)
4550
vbox.Add(self.find_text, 0, wx.EXPAND|wx.ALL, 5)
4651

4752
vbox.Add(wx.StaticText(panel, label="Replace with:"), 0, wx.ALL, 5)
48-
self.replace_text = wx.TextCtrl(panel)
53+
self.replace_text = wx.TextCtrl(panel, style=wx.TE_PROCESS_ENTER)
4954
vbox.Add(self.replace_text, 0, wx.EXPAND|wx.ALL, 5)
5055

56+
# --- NEW: Direction controls ---
57+
self.direction_radio = wx.RadioBox(panel, label="Direction", choices=["Down", "Up"])
58+
vbox.Add(self.direction_radio, 0, wx.EXPAND|wx.ALL, 5)
59+
5160
self.case_checkbox = wx.CheckBox(panel, label="Match case")
5261
vbox.Add(self.case_checkbox, 0, wx.ALL, 5)
5362

5463
hbox = wx.BoxSizer(wx.HORIZONTAL)
55-
find_next_btn = wx.Button(panel, label="Find Next")
56-
replace_btn = wx.Button(panel, label="Replace")
57-
replace_all_btn = wx.Button(panel, label="Replace All")
58-
close_btn = wx.Button(panel, label="Close", id=wx.ID_CANCEL) # Use standard Cancel ID
64+
find_next_btn = wx.Button(panel, label="Find &Next")
65+
replace_btn = wx.Button(panel, label="&Replace")
66+
replace_all_btn = wx.Button(panel, label="Replace &All")
67+
close_btn = wx.Button(panel, label="&Close", id=wx.ID_CANCEL)
5968
hbox.Add(find_next_btn)
6069
hbox.Add(replace_btn, 0, wx.LEFT, 5)
6170
hbox.Add(replace_all_btn, 0, wx.LEFT, 5)
62-
hbox.Add(close_btn, 0, wx.LEFT, 15) # Add extra space
71+
hbox.Add(close_btn, 0, wx.LEFT, 15)
6372
vbox.Add(hbox, 0, wx.EXPAND|wx.ALL, 5)
6473

6574
panel.SetSizer(vbox)
6675
find_next_btn.Bind(wx.EVT_BUTTON, self.on_find_next)
6776
replace_btn.Bind(wx.EVT_BUTTON, self.on_replace)
6877
replace_all_btn.Bind(wx.EVT_BUTTON, self.on_replace_all)
78+
self.find_text.Bind(wx.EVT_TEXT_ENTER, self.on_find_next)
79+
self.replace_text.Bind(wx.EVT_TEXT_ENTER, self.on_find_next)
6980
self.find_text.SetFocus()
7081

7182
def on_find_next(self, event):
72-
self.parent.do_find(self.find_text.GetValue(), self.case_checkbox.IsChecked())
83+
direction = self.direction_radio.GetStringSelection().lower()
84+
self.parent.do_find(self.find_text.GetValue(), self.case_checkbox.IsChecked(), direction)
7385

7486
def on_replace(self, event):
75-
self.parent.do_replace(self.find_text.GetValue(), self.replace_text.GetValue(), self.case_checkbox.IsChecked())
87+
direction = self.direction_radio.GetStringSelection().lower()
88+
self.parent.do_replace(self.find_text.GetValue(), self.replace_text.GetValue(), self.case_checkbox.IsChecked(), direction)
7689

7790
def on_replace_all(self, event):
7891
self.parent.do_replace_all(self.find_text.GetValue(), self.replace_text.GetValue(), self.case_checkbox.IsChecked())
7992

80-
8193
class EditorFrame(wx.Frame, SettingsMenuMixin):
94+
# ... (__init__ is updated for new menu items) ...
8295
def __init__(self, parent, title, local_path, remote_path, sftp_client):
8396
super(EditorFrame, self).__init__(parent, title=title, size=(800, 600))
8497
self.local_path, self.remote_path, self.sftp = local_path, remote_path, sftp_client
@@ -100,6 +113,7 @@ def __init__(self, parent, title, local_path, remote_path, sftp_client):
100113
paste_item = edit_menu.Append(wx.ID_PASTE, "&Paste\tCtrl+V")
101114
search_menu = wx.Menu()
102115
find_item = search_menu.Append(wx.ID_FIND, "&Find\tCtrl+F")
116+
find_next_item = search_menu.Append(wx.ID_ANY, "Find &Next\tF3") # NEW
103117
replace_item = search_menu.Append(wx.ID_REPLACE, "Find and &Replace\tCtrl+H")
104118
goto_item = search_menu.Append(wx.ID_ANY, "&Go To Line\tCtrl+G")
105119
menu_bar.Append(file_menu, "&File")
@@ -117,6 +131,7 @@ def __init__(self, parent, title, local_path, remote_path, sftp_client):
117131
self.Bind(wx.EVT_MENU, lambda e: self.text_ctrl.Copy(), copy_item)
118132
self.Bind(wx.EVT_MENU, lambda e: self.text_ctrl.Paste(), paste_item)
119133
self.Bind(wx.EVT_MENU, self.on_find, find_item)
134+
self.Bind(wx.EVT_MENU, lambda e: self.do_find(self.last_search_string, "matchcase" in self.last_search_flags, "down"), find_next_item)
120135
self.Bind(wx.EVT_MENU, self.on_replace, replace_item)
121136
self.Bind(wx.EVT_MENU, self.on_go_to_line, goto_item)
122137
self.Bind(wx.EVT_CLOSE, self.on_close)
@@ -125,37 +140,51 @@ def __init__(self, parent, title, local_path, remote_path, sftp_client):
125140
self.load_file_content()
126141
self.Show()
127142

128-
def on_find(self, event):
129-
with FindDialog(self, "Find") as dlg:
130-
dlg.ShowModal()
131-
def on_replace(self, event):
132-
with ReplaceDialog(self, "Find and Replace") as dlg:
133-
dlg.ShowModal()
134-
def do_find(self, find_string, match_case):
143+
# --- UPDATED: `do_find` now handles direction ---
144+
def do_find(self, find_string, match_case, direction="down"):
145+
if not find_string: return
135146
self.last_search_string = find_string
136-
flags = wx.FR_DOWN
137-
if match_case: flags |= wx.FR_MATCHCASE
138-
self.last_search_flags = flags
147+
139148
content = self.text_ctrl.GetValue()
149+
search_content = content if match_case else content.lower()
150+
search_string = find_string if match_case else find_string.lower()
151+
140152
start = self.text_ctrl.GetInsertionPoint()
141-
if not match_case:
142-
content = content.lower()
143-
find_string = find_string.lower()
144-
pos = content.find(find_string, start)
145-
if pos == -1: pos = content.find(find_string, 0)
153+
154+
pos = -1
155+
if direction == "down":
156+
pos = search_content.find(search_string, start)
157+
if pos == -1: # Wrap search
158+
pos = search_content.find(search_string, 0)
159+
else: # direction == "up"
160+
pos = search_content.rfind(search_string, 0, start)
161+
if pos == -1: # Wrap search
162+
pos = search_content.rfind(search_string)
163+
146164
if pos != -1:
147165
self.text_ctrl.SetSelection(pos, pos + len(find_string))
148166
self.text_ctrl.SetFocus()
149167
else:
150168
wx.MessageBox(f"'{find_string}' not found.", "Find", wx.ICON_INFORMATION)
151-
def do_replace(self, find_string, replace_string, match_case):
169+
170+
# --- UPDATED: `do_replace` now passes direction ---
171+
def do_replace(self, find_string, replace_string, match_case, direction="down"):
152172
start, end = self.text_ctrl.GetSelection()
153173
selected_text = self.text_ctrl.GetStringSelection()
174+
154175
compare_text = selected_text if match_case else selected_text.lower()
155176
compare_find = find_string if match_case else find_string.lower()
177+
156178
if compare_text == compare_find:
157179
self.text_ctrl.Replace(start, end, replace_string)
158-
self.do_find(find_string, match_case)
180+
181+
self.do_find(find_string, match_case, direction)
182+
183+
# ... (rest of file is unchanged) ...
184+
def on_find(self, event):
185+
with FindDialog(self, "Find") as dlg: dlg.ShowModal()
186+
def on_replace(self, event):
187+
with ReplaceDialog(self, "Find and Replace") as dlg: dlg.ShowModal()
159188
def do_replace_all(self, find_string, replace_string, match_case):
160189
content = self.text_ctrl.GetValue()
161190
if match_case:

0 commit comments

Comments
 (0)