PyBreeze 1.0.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. automation_ide/__init__.py +7 -0
  2. automation_ide/__main__.py +0 -0
  3. automation_ide/automation_editor_ui/__init__.py +0 -0
  4. automation_ide/automation_editor_ui/connect_gui/__init__.py +0 -0
  5. automation_ide/automation_editor_ui/connect_gui/ssh/__init__.py +0 -0
  6. automation_ide/automation_editor_ui/connect_gui/ssh/ssh_command_widget.py +246 -0
  7. automation_ide/automation_editor_ui/connect_gui/ssh/ssh_file_viewer_widget.py +501 -0
  8. automation_ide/automation_editor_ui/connect_gui/ssh/ssh_login_widget.py +67 -0
  9. automation_ide/automation_editor_ui/connect_gui/ssh/ssh_main_widget.py +49 -0
  10. automation_ide/automation_editor_ui/connect_gui/url/__init__.py +0 -0
  11. automation_ide/automation_editor_ui/connect_gui/url/ai_code_review_gui.py +177 -0
  12. automation_ide/automation_editor_ui/editor_main/__init__.py +0 -0
  13. automation_ide/automation_editor_ui/editor_main/main_ui.py +104 -0
  14. automation_ide/automation_editor_ui/extend_ai_gui/__init__.py +0 -0
  15. automation_ide/automation_editor_ui/extend_ai_gui/ai_gui_global_variable.py +40 -0
  16. automation_ide/automation_editor_ui/extend_ai_gui/code_review/__init__.py +0 -0
  17. automation_ide/automation_editor_ui/extend_ai_gui/code_review/code_review_thread.py +81 -0
  18. automation_ide/automation_editor_ui/extend_ai_gui/code_review/cot_code_review_gui.py +82 -0
  19. automation_ide/automation_editor_ui/extend_ai_gui/prompt_edit_gui/__init__.py +0 -0
  20. automation_ide/automation_editor_ui/extend_ai_gui/prompt_edit_gui/cot_code_review_prompt_templates/__init__.py +0 -0
  21. automation_ide/automation_editor_ui/extend_ai_gui/prompt_edit_gui/cot_code_review_prompt_templates/code_smell_detector.py +22 -0
  22. automation_ide/automation_editor_ui/extend_ai_gui/prompt_edit_gui/cot_code_review_prompt_templates/first_code_review.py +17 -0
  23. automation_ide/automation_editor_ui/extend_ai_gui/prompt_edit_gui/cot_code_review_prompt_templates/first_summary_prompt.py +17 -0
  24. automation_ide/automation_editor_ui/extend_ai_gui/prompt_edit_gui/cot_code_review_prompt_templates/global_rule.py +59 -0
  25. automation_ide/automation_editor_ui/extend_ai_gui/prompt_edit_gui/cot_code_review_prompt_templates/judge.py +91 -0
  26. automation_ide/automation_editor_ui/extend_ai_gui/prompt_edit_gui/cot_code_review_prompt_templates/judge_single_review.py +85 -0
  27. automation_ide/automation_editor_ui/extend_ai_gui/prompt_edit_gui/cot_code_review_prompt_templates/linter.py +17 -0
  28. automation_ide/automation_editor_ui/extend_ai_gui/prompt_edit_gui/cot_code_review_prompt_templates/step_by_step_analysis.py +35 -0
  29. automation_ide/automation_editor_ui/extend_ai_gui/prompt_edit_gui/cot_code_review_prompt_templates/total_summary.py +66 -0
  30. automation_ide/automation_editor_ui/extend_ai_gui/prompt_edit_gui/cot_prompt_editor_widget.py +136 -0
  31. automation_ide/automation_editor_ui/extend_ai_gui/prompt_edit_gui/skills_prompt_editor_widget.py +136 -0
  32. automation_ide/automation_editor_ui/extend_ai_gui/prompt_edit_gui/skills_prompt_templates/__init__.py +0 -0
  33. automation_ide/automation_editor_ui/extend_ai_gui/prompt_edit_gui/skills_prompt_templates/code_explainer.py +21 -0
  34. automation_ide/automation_editor_ui/extend_ai_gui/prompt_edit_gui/skills_prompt_templates/code_review.py +37 -0
  35. automation_ide/automation_editor_ui/extend_ai_gui/skills/__init__.py +0 -0
  36. automation_ide/automation_editor_ui/extend_ai_gui/skills/skills_send_gui.py +98 -0
  37. automation_ide/automation_editor_ui/extend_multi_language/__init__.py +0 -0
  38. automation_ide/automation_editor_ui/extend_multi_language/extend_english.py +272 -0
  39. automation_ide/automation_editor_ui/extend_multi_language/extend_traditional_chinese.py +272 -0
  40. automation_ide/automation_editor_ui/extend_multi_language/update_language_dict.py +8 -0
  41. automation_ide/automation_editor_ui/menu/__init__.py +0 -0
  42. automation_ide/automation_editor_ui/menu/automation_menu/__init__.py +0 -0
  43. automation_ide/automation_editor_ui/menu/automation_menu/api_testka_menu/__init__.py +0 -0
  44. automation_ide/automation_editor_ui/menu/automation_menu/api_testka_menu/build_api_testka_menu.py +137 -0
  45. automation_ide/automation_editor_ui/menu/automation_menu/auto_control_menu/__init__.py +0 -0
  46. automation_ide/automation_editor_ui/menu/automation_menu/auto_control_menu/build_autocontrol_menu.py +169 -0
  47. automation_ide/automation_editor_ui/menu/automation_menu/automation_file_menu/__init__.py +0 -0
  48. automation_ide/automation_editor_ui/menu/automation_menu/automation_file_menu/build_automation_file_menu.py +122 -0
  49. automation_ide/automation_editor_ui/menu/automation_menu/load_density_menu/__init__.py +0 -0
  50. automation_ide/automation_editor_ui/menu/automation_menu/load_density_menu/build_load_density_menu.py +137 -0
  51. automation_ide/automation_editor_ui/menu/automation_menu/mail_thunder_menu/__init__.py +0 -0
  52. automation_ide/automation_editor_ui/menu/automation_menu/mail_thunder_menu/build_mail_thunder_menu.py +88 -0
  53. automation_ide/automation_editor_ui/menu/automation_menu/test_pioneer_menu/__init__.py +0 -0
  54. automation_ide/automation_editor_ui/menu/automation_menu/test_pioneer_menu/build_test_pioneer_menu.py +62 -0
  55. automation_ide/automation_editor_ui/menu/automation_menu/web_runner_menu/__init__.py +0 -0
  56. automation_ide/automation_editor_ui/menu/automation_menu/web_runner_menu/build_webrunner_menu.py +119 -0
  57. automation_ide/automation_editor_ui/menu/build_menubar.py +47 -0
  58. automation_ide/automation_editor_ui/menu/install_menu/__init__.py +0 -0
  59. automation_ide/automation_editor_ui/menu/install_menu/automation_menu/__init__.py +0 -0
  60. automation_ide/automation_editor_ui/menu/install_menu/automation_menu/build_automation_install_menu.py +82 -0
  61. automation_ide/automation_editor_ui/menu/install_menu/install_utils.py +21 -0
  62. automation_ide/automation_editor_ui/menu/install_menu/tools_menu/__init__.py +0 -0
  63. automation_ide/automation_editor_ui/menu/install_menu/tools_menu/build_tool_install_menu.py +29 -0
  64. automation_ide/automation_editor_ui/menu/menu_utils.py +16 -0
  65. automation_ide/automation_editor_ui/menu/tools/__init__.py +0 -0
  66. automation_ide/automation_editor_ui/menu/tools/tools_menu.py +162 -0
  67. automation_ide/automation_editor_ui/show_code_window/__init__.py +0 -0
  68. automation_ide/automation_editor_ui/show_code_window/code_window.py +21 -0
  69. automation_ide/automation_editor_ui/syntax/__init__.py +0 -0
  70. automation_ide/automation_editor_ui/syntax/syntax_extend.py +40 -0
  71. automation_ide/automation_editor_ui/syntax/syntax_keyword.py +163 -0
  72. automation_ide/extend/__init__.py +0 -0
  73. automation_ide/extend/mail_thunder_extend/__init__.py +0 -0
  74. automation_ide/extend/mail_thunder_extend/mail_thunder_setting.py +36 -0
  75. automation_ide/extend/process_executor/__init__.py +0 -0
  76. automation_ide/extend/process_executor/api_testka/__init__.py +0 -0
  77. automation_ide/extend/process_executor/api_testka/api_testka_process.py +66 -0
  78. automation_ide/extend/process_executor/auto_control/__init__.py +0 -0
  79. automation_ide/extend/process_executor/auto_control/auto_control_process.py +62 -0
  80. automation_ide/extend/process_executor/file_automation/__init__.py +0 -0
  81. automation_ide/extend/process_executor/file_automation/file_automation_process.py +66 -0
  82. automation_ide/extend/process_executor/load_density/__init__.py +0 -0
  83. automation_ide/extend/process_executor/load_density/load_density_process.py +66 -0
  84. automation_ide/extend/process_executor/mail_thunder/__init__.py +0 -0
  85. automation_ide/extend/process_executor/mail_thunder/mail_thunder_process.py +16 -0
  86. automation_ide/extend/process_executor/process_executor_utils.py +72 -0
  87. automation_ide/extend/process_executor/python_task_process_manager.py +177 -0
  88. automation_ide/extend/process_executor/test_pioneer/__init__.py +0 -0
  89. automation_ide/extend/process_executor/test_pioneer/test_pioneer_process_manager.py +181 -0
  90. automation_ide/extend/process_executor/web_runner/__init__.py +0 -0
  91. automation_ide/extend/process_executor/web_runner/web_runner_process.py +66 -0
  92. automation_ide/utils/__init__.py +0 -0
  93. automation_ide/utils/exception/__init__.py +0 -0
  94. automation_ide/utils/exception/exception_tags.py +37 -0
  95. automation_ide/utils/exception/exceptions.py +50 -0
  96. automation_ide/utils/file_process/__init__.py +0 -0
  97. automation_ide/utils/file_process/get_dir_file_list.py +29 -0
  98. automation_ide/utils/json_format/__init__.py +1 -0
  99. automation_ide/utils/json_format/json_process.py +28 -0
  100. automation_ide/utils/logging/__init__.py +0 -0
  101. automation_ide/utils/logging/logger.py +50 -0
  102. automation_ide/utils/manager/__init__.py +0 -0
  103. automation_ide/utils/manager/package_manager/__init__.py +0 -0
  104. automation_ide/utils/manager/package_manager/package_manager_class.py +18 -0
  105. pybreeze-1.0.4.dist-info/METADATA +69 -0
  106. pybreeze-1.0.4.dist-info/RECORD +109 -0
  107. pybreeze-1.0.4.dist-info/WHEEL +5 -0
  108. pybreeze-1.0.4.dist-info/licenses/LICENSE +21 -0
  109. pybreeze-1.0.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,501 @@
1
+ import os
2
+ from pathlib import Path
3
+ from typing import Optional
4
+
5
+ import paramiko
6
+ from PySide6.QtCore import Qt, QEvent
7
+ from PySide6.QtWidgets import (
8
+ QWidget, QVBoxLayout, QLineEdit, QPushButton, QTreeWidget, QTreeWidgetItem,
9
+ QMenu, QFileDialog, QMessageBox, QSplitter, QInputDialog, QStyle
10
+ )
11
+ from je_editor import language_wrapper
12
+
13
+ from automation_ide.automation_editor_ui.connect_gui.ssh.ssh_login_widget import LoginWidget
14
+
15
+
16
+ class SFTPClientWrapper:
17
+ """
18
+ Lightweight wrapper around Paramiko SFTP client.
19
+ 輕量級封裝 Paramiko SFTP 客戶端,提供基本操作。
20
+ """
21
+
22
+ def __init__(self):
23
+ self.word_dict = language_wrapper.language_word_dict
24
+ self._ssh: Optional[paramiko.SSHClient] = None
25
+ self._sftp: Optional[paramiko.SFTPClient] = None
26
+ self.root_path: str = "/"
27
+
28
+ def connect(self, host: str, port: int, username: str, password: str):
29
+ """
30
+ Establish SSH + SFTP connection.
31
+ 建立 SSH + SFTP 連線。
32
+ """
33
+ self.close()
34
+ self._ssh = paramiko.SSHClient()
35
+ self._ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
36
+ self._ssh.connect(hostname=host, port=port, username=username, password=password)
37
+ self._sftp = self._ssh.open_sftp()
38
+
39
+ def close(self):
40
+ """
41
+ Close SFTP and SSH safely.
42
+ 安全關閉 SFTP 與 SSH。
43
+ """
44
+ try:
45
+ if self._sftp:
46
+ self._sftp.close()
47
+ finally:
48
+ self._sftp = None
49
+ try:
50
+ if self._ssh:
51
+ self._ssh.close()
52
+ finally:
53
+ self._ssh = None
54
+
55
+ @property
56
+ def connected(self) -> bool:
57
+ """
58
+ Check connection state.
59
+ 檢查連線狀態。
60
+ """
61
+ return self._ssh is not None and self._sftp is not None
62
+
63
+ def list_dir(self, path: str):
64
+ """
65
+ List directory entries with stat attributes.
66
+ 列出目錄項目(含屬性)。
67
+ """
68
+ if not self.connected:
69
+ raise RuntimeError(
70
+ self.word_dict.get("ssh_command_widget_dialog_title_not_connected")
71
+ )
72
+ return self._sftp.listdir_attr(path)
73
+
74
+ def is_dir(self, path: str) -> bool:
75
+ """
76
+ Determine if target path is a directory.
77
+ 判斷路徑是否為目錄。
78
+ """
79
+ if not self.connected:
80
+ raise RuntimeError(self.word_dict.get(
81
+ "ssh_command_widget_dialog_title_not_connected"
82
+ ))
83
+ try:
84
+ st = self._sftp.stat(path)
85
+ # S_ISDIR check via stat.S_ISDIR
86
+ import stat
87
+ return stat.S_ISDIR(st.st_mode)
88
+ except IOError:
89
+ return False
90
+
91
+ def mkdir(self, path: str):
92
+ """
93
+ Create directory.
94
+ 建立目錄。
95
+ """
96
+ self._sftp.mkdir(path)
97
+
98
+ def remove_file(self, path: str):
99
+ """
100
+ Remove file.
101
+ 刪除檔案。
102
+ """
103
+ self._sftp.remove(path)
104
+
105
+ def remove_dir(self, path: str):
106
+ """
107
+ Remove empty directory.
108
+ 刪除空目錄。
109
+ """
110
+ self._sftp.rmdir(path)
111
+
112
+ def rename(self, old_path: str, new_path: str):
113
+ """
114
+ Rename file/folder.
115
+ 重新命名檔案/資料夾。
116
+ """
117
+ self._sftp.rename(old_path, new_path)
118
+
119
+ def download(self, remote_path: str, local_path: str):
120
+ """
121
+ Download remote to local.
122
+ 下載遠端檔案至本地。
123
+ """
124
+ self._sftp.get(remote_path, local_path)
125
+
126
+ def upload(self, local_path: str, remote_path: str):
127
+ """
128
+ Upload local to remote.
129
+ 上傳本地檔案至遠端。
130
+ """
131
+ self._sftp.put(local_path, remote_path)
132
+
133
+
134
+ class SSHFileTreeManager(QWidget):
135
+ """
136
+ QWidget: connection form + tree + context menu.
137
+ QWidget:連線表單 + 樹狀檔案管理 + 右鍵選單。
138
+ """
139
+
140
+ def __init__(self, external_login_widget: LoginWidget = None, add_login_widget: bool = True):
141
+ super().__init__()
142
+ self.word_dict = language_wrapper.language_word_dict
143
+ self.setWindowTitle(
144
+ self.word_dict.get("ssh_file_viewer_window_title_file_tree_manager")
145
+ )
146
+ self.add_login_widget = add_login_widget
147
+
148
+ self.client = SFTPClientWrapper()
149
+
150
+ if self.add_login_widget:
151
+ # 使用獨立的登入介面
152
+ self.login_widget = LoginWidget()
153
+ else:
154
+ if external_login_widget is None:
155
+ external_login_widget = LoginWidget()
156
+ self.login_widget = external_login_widget
157
+
158
+ # UI: Tree
159
+ self.tree = QTreeWidget()
160
+ self.tree.setHeaderLabels(["Name", "Type", "Size", "Path"])
161
+ self.tree.itemExpanded.connect(self.on_item_expanded)
162
+ self.tree.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
163
+ self.tree.customContextMenuRequested.connect(self.on_context_menu)
164
+
165
+ # Layouts
166
+ splitter = QSplitter(Qt.Orientation.Vertical)
167
+ splitter.addWidget(self.login_widget) # 插入登入介面
168
+ splitter.addWidget(self.tree)
169
+ splitter.setStretchFactor(1, 1)
170
+
171
+ container = QWidget()
172
+ root_layout = QVBoxLayout(container)
173
+ root_layout.addWidget(splitter)
174
+ self.setLayout(root_layout)
175
+
176
+ # Signals
177
+ self.login_widget.connect_btn.clicked.connect(self._connect)
178
+ self.login_widget.disconnect_btn.clicked.connect(self._disconnect)
179
+
180
+ def _connect(self):
181
+ """
182
+ Connect to SSH and load root.
183
+ 連線 SSH 並載入根目錄。
184
+ """
185
+ host = self.login_widget.host_edit.text().strip()
186
+ port = int(self.login_widget.port_spin.text().strip() or "22")
187
+ user = self.login_widget.user_edit.text().strip()
188
+ pwd = self.login_widget.pass_edit.text()
189
+
190
+ if not host or not user or not pwd:
191
+ QMessageBox.warning(
192
+ self,
193
+ self.word_dict.get("ssh_file_viewer_dialog_title_missing_input"),
194
+ self.word_dict.get("ssh_file_viewer_dialog_message_missing_input"))
195
+ return
196
+ try:
197
+ self.client.connect(host, port, user, pwd)
198
+ self.load_root("/")
199
+ except Exception as e:
200
+ QMessageBox.critical(
201
+ self,
202
+ self.word_dict.get("ssh_file_viewer_dialog_title_connection_failed"),
203
+ f"{self.word_dict.get('ssh_file_viewer_dialog_message_connection_failed')}: {e}")
204
+
205
+ def _disconnect(self):
206
+ """
207
+ Disconnect SSH.
208
+ 斷線 SSH。
209
+ """
210
+ self.client.close()
211
+ self.tree.clear()
212
+
213
+ def load_root(self, path: str = "/"):
214
+ """
215
+ Clear and load root items.
216
+ 清空並載入根項目。
217
+ """
218
+ self.tree.clear()
219
+ root_item = self.make_item("/", "dir", 0, "/")
220
+ # Add a placeholder child to show expandable icon
221
+ self.add_placeholder(root_item)
222
+ self.tree.addTopLevelItem(root_item)
223
+ root_item.setExpanded(True)
224
+ self.populate_children(root_item)
225
+
226
+ def make_item(self, name: str, typ: str, size: int, full_path: str) -> QTreeWidgetItem:
227
+ """
228
+ Create a tree item with metadata.
229
+ 建立帶有中繼資料的樹狀項目。
230
+ """
231
+ item = QTreeWidgetItem([name, typ, str(size), full_path])
232
+ if typ == "dir":
233
+ # Use QStyle enum for standard icons
234
+ # 使用 QStyle 列舉取得標準資料夾圖示
235
+ item.setIcon(0, self.style().standardIcon(QStyle.StandardPixmap.SP_DirIcon))
236
+ else:
237
+ # Use QStyle enum for standard file icon
238
+ # 使用 QStyle 列舉取得標準檔案圖示
239
+ item.setIcon(0, self.style().standardIcon(QStyle.StandardPixmap.SP_FileIcon))
240
+ return item
241
+
242
+ def add_placeholder(self, item: QTreeWidgetItem):
243
+ """
244
+ Add a dummy child to indicate lazy-load.
245
+ 加入占位子項以表示可延遲載入。
246
+ """
247
+ placeholder = QTreeWidgetItem(["...", "", "", ""])
248
+ item.addChild(placeholder)
249
+
250
+ def is_placeholder_present(self, item: QTreeWidgetItem) -> bool:
251
+ """
252
+ Check if the first child is a placeholder.
253
+ 檢查第一個子項是否為占位項。
254
+ """
255
+ if item.childCount() == 0:
256
+ return False
257
+ return item.child(0).text(0) == "..."
258
+
259
+ def on_item_expanded(self, item: QTreeWidgetItem):
260
+ """
261
+ When a directory is expanded, populate its children.
262
+ 展開目錄時載入子項目。
263
+ """
264
+ # Only load once
265
+ if self.is_placeholder_present(item):
266
+ # Remove placeholder then populate
267
+ item.takeChild(0)
268
+ self.populate_children(item)
269
+
270
+ def populate_children(self, parent_item: QTreeWidgetItem):
271
+ """
272
+ List items under parent path and add to tree.
273
+ 列出父路徑下的項目並加入樹。
274
+ """
275
+ if not self.client.connected:
276
+ return
277
+ path = parent_item.text(3)
278
+ try:
279
+ entries = self.client.list_dir(path)
280
+ # Sort: dirs first, then files
281
+ import stat
282
+ dirs = []
283
+ files = []
284
+ for e in entries:
285
+ name = e.filename
286
+ full_path = os.path.join(path if path != "/" else "", name)
287
+ full_path = full_path if full_path.startswith("/") else f"/{full_path}"
288
+ is_dir = stat.S_ISDIR(e.st_mode)
289
+ if is_dir:
290
+ dirs.append((name, e))
291
+ else:
292
+ files.append((name, e))
293
+ for name, e in dirs + files:
294
+ import stat as _stat
295
+ full_path = os.path.join(path if path != "/" else "", name)
296
+ full_path = full_path if full_path.startswith("/") else f"/{full_path}"
297
+ typ = "dir" if _stat.S_ISDIR(e.st_mode) else "file"
298
+ size = e.st_size if typ == "file" else 0
299
+ child = self.make_item(name, typ, size, full_path)
300
+ parent_item.addChild(child)
301
+ if typ == "dir":
302
+ self.add_placeholder(child)
303
+ except Exception as ex:
304
+ QMessageBox.critical(
305
+ self,
306
+ self.word_dict.get("ssh_file_viewer_dialog_title_list_error"),
307
+ f"{self.word_dict.get('ssh_file_viewer_dialog_message_list_failed')} '{path}': {ex}")
308
+
309
+ def on_context_menu(self, pos):
310
+ """
311
+ Show context menu for file operations.
312
+ 顯示右鍵選單以進行檔案操作。
313
+ """
314
+ item = self.tree.itemAt(pos)
315
+ menu = QMenu(self)
316
+
317
+ refresh_act = menu.addAction("Refresh")
318
+ create_act = menu.addAction("Create folder")
319
+ rename_act = menu.addAction("Rename")
320
+ delete_act = menu.addAction("Delete")
321
+ download_act = menu.addAction("Download")
322
+ upload_act = menu.addAction("Upload to this folder")
323
+
324
+ action = menu.exec_(self.tree.viewport().mapToGlobal(pos))
325
+ if action is None:
326
+ return
327
+
328
+ try:
329
+ if action == refresh_act:
330
+ self.action_refresh(item)
331
+ elif action == create_act:
332
+ self.action_create_folder(item)
333
+ elif action == rename_act:
334
+ self.action_rename(item)
335
+ elif action == delete_act:
336
+ self.action_delete(item)
337
+ elif action == download_act:
338
+ self.action_download(item)
339
+ elif action == upload_act:
340
+ self.action_upload(item)
341
+ except Exception as e:
342
+ QMessageBox.critical(
343
+ self,
344
+ self.word_dict.get("ssh_file_viewer_dialog_title_operation_failed"),
345
+ f"{self.word_dict.get('ssh_file_viewer_dialog_message_operation_failed')}: {e}")
346
+
347
+ def action_refresh(self, item: Optional[QTreeWidgetItem]):
348
+ """
349
+ Refresh current item children (or root).
350
+ 重新整理目前項目的子項(或根)。
351
+ """
352
+ target = item or (self.tree.topLevelItem(0) if self.tree.topLevelItemCount() else None)
353
+ if target is None:
354
+ return
355
+ # Clear and reload
356
+ target.takeChildren()
357
+ if target.text(1) == "dir":
358
+ self.add_placeholder(target)
359
+ self.on_item_expanded(target)
360
+
361
+ def action_create_folder(self, item: Optional[QTreeWidgetItem]):
362
+ """
363
+ Create a subfolder under the selected directory.
364
+ 在選定目錄下建立子資料夾。
365
+ """
366
+ if item is None:
367
+ QMessageBox.information(
368
+ self,
369
+ self.word_dict.get("ssh_file_viewer_dialog_title_no_selection"),
370
+ self.word_dict.get("ssh_file_viewer_dialog_message_select_folder_to_create"))
371
+ return
372
+ base_path = item.text(3)
373
+ if item.text(1) != "dir":
374
+ base_path = os.path.dirname(base_path)
375
+ name, ok = self.get_text(
376
+ self.word_dict.get("ssh_file_viewer_dialog_title_create_folder"),
377
+ self.word_dict.get("ssh_file_viewer_dialog_label_folder_name"))
378
+ if not ok or not name.strip():
379
+ return
380
+ new_path = os.path.join(base_path if base_path != "/" else "", name.strip())
381
+ new_path = new_path if new_path.startswith("/") else f"/{new_path}"
382
+ self.client.mkdir(new_path)
383
+ self.action_refresh(item)
384
+
385
+ def action_rename(self, item: Optional[QTreeWidgetItem]):
386
+ """
387
+ Rename selected item.
388
+ 重新命名選定項目。
389
+ """
390
+ if item is None:
391
+ return
392
+ old_path = item.text(3)
393
+ new_name, ok = self.get_text(
394
+ self.word_dict.get("ssh_file_viewer_dialog_title_rename"),
395
+ f"{self.word_dict.get('ssh_file_viewer_dialog_label_new_name_for_item')}: {item.text(0)}")
396
+ if not ok or not new_name.strip():
397
+ return
398
+ base = os.path.dirname(old_path) or "/"
399
+ new_path = os.path.join(base if base != "/" else "", new_name.strip())
400
+ new_path = new_path if new_path.startswith("/") else f"/{new_path}"
401
+ self.client.rename(old_path, new_path)
402
+ # Update item display
403
+ item.setText(0, new_name.strip())
404
+ item.setText(3, new_path)
405
+
406
+ def action_delete(self, item: Optional[QTreeWidgetItem]):
407
+ """
408
+ Delete selected file/folder (folder must be empty).
409
+ 刪除選定檔案/資料夾(資料夾需為空)。
410
+ """
411
+ if item is None:
412
+ return
413
+ path = item.text(3)
414
+ reply = QMessageBox.question(
415
+ self,
416
+ self.word_dict.get("ssh_file_viewer_dialog_title_confirm_delete"),
417
+ f"{self.word_dict.get('ssh_file_viewer_dialog_message_confirm_delete')} '{path}'?",
418
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
419
+ )
420
+ if reply != QMessageBox.StandardButton.Yes:
421
+ return
422
+ # Try dir first; if fails, try file
423
+ if item.text(1) == "dir":
424
+ self.client.remove_dir(path)
425
+ else:
426
+ self.client.remove_file(path)
427
+ parent = item.parent()
428
+ if parent:
429
+ parent.removeChild(item)
430
+ else:
431
+ self.tree.takeTopLevelItem(self.tree.indexOfTopLevelItem(item))
432
+
433
+ def action_download(self, item: Optional[QTreeWidgetItem]):
434
+ """
435
+ Download selected file to local.
436
+ 將選定檔案下載至本地。
437
+ """
438
+ if item is None or item.text(1) != "file":
439
+ QMessageBox.information(
440
+ self,
441
+ self.word_dict.get("ssh_file_viewer_dialog_title_invalid_selection"),
442
+ self.word_dict.get("ssh_file_viewer_dialog_message_select_file_to_download"))
443
+ return
444
+ remote_path = item.text(3)
445
+ suggested = Path(remote_path).name
446
+ local_path, _ = QFileDialog.getSaveFileName(
447
+ self,
448
+ self.word_dict.get("ssh_file_viewer_dialog_title_save_as"),
449
+ suggested)
450
+ if not local_path:
451
+ return
452
+ self.client.download(remote_path, local_path)
453
+ QMessageBox.information(
454
+ self,
455
+ self.word_dict.get("ssh_file_viewer_dialog_title_downloaded"),
456
+ f"{self.word_dict.get('ssh_file_viewer_dialog_message_saved_to')}: {local_path}")
457
+
458
+ def action_upload(self, item: Optional[QTreeWidgetItem]):
459
+ """
460
+ Upload a local file into the selected folder.
461
+ 將本地檔案上傳至所選資料夾。
462
+ """
463
+ if item is None:
464
+ return
465
+ target_dir = item.text(3) if item.text(1) == "dir" else os.path.dirname(item.text(3))
466
+ local_path, _ = QFileDialog.getOpenFileName(
467
+ self, self.word_dict.get("ssh_file_viewer_dialog_title_select_local_file"), "")
468
+ if not local_path:
469
+ return
470
+ filename = os.path.basename(local_path)
471
+ remote_path = os.path.join(target_dir if target_dir != "/" else "", filename)
472
+ remote_path = remote_path if remote_path.startswith("/") else f"/{remote_path}"
473
+ self.client.upload(local_path, remote_path)
474
+ QMessageBox.information(
475
+ self,
476
+ self.word_dict.get("ssh_file_viewer_dialog_title_uploaded"),
477
+ f"{self.word_dict.get('ssh_file_viewer_dialog_message_uploaded_to')}: {remote_path}")
478
+ # Refresh folder contents
479
+ self.action_refresh(item)
480
+
481
+ def get_text(self, title: str, label: str):
482
+ """
483
+ Simple input dialog using QMessageBox alternative.
484
+ 簡易文字輸入對話框(基於 QLineEdit)。
485
+ """
486
+ text, ok = QInputDialog.getText(self, title, label)
487
+ return text, ok
488
+
489
+ def eventFilter(self, obj, event):
490
+ """
491
+ Allow Enter to trigger OK in our improvised input dialog.
492
+ 允許在自製輸入框中使用 Enter 觸發確定。
493
+ """
494
+ if isinstance(obj, QLineEdit) and event.type() == QEvent.Type.KeyPress:
495
+ if event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
496
+ w = obj.window()
497
+ for b in w.findChildren(QPushButton):
498
+ if b.text().lower() in (self.word_dict.get("ssh_file_viewer_dialog_button_ok"),):
499
+ b.click()
500
+ return True
501
+ return super().eventFilter(obj, event)
@@ -0,0 +1,67 @@
1
+ from PySide6.QtWidgets import (
2
+ QWidget, QHBoxLayout, QVBoxLayout, QLabel,
3
+ QLineEdit, QSpinBox, QCheckBox, QPushButton
4
+ )
5
+ from je_editor import language_wrapper
6
+
7
+
8
+ class LoginWidget(QWidget):
9
+ """登入介面獨立 QWidget"""
10
+
11
+ def __init__(self, parent=None):
12
+ super().__init__(parent)
13
+
14
+ # UI 控制元件
15
+ self.host_edit = QLineEdit()
16
+ self.port_spin = QSpinBox()
17
+ self.user_edit = QLineEdit()
18
+ self.pass_edit = QLineEdit()
19
+ self.key_edit = QLineEdit()
20
+ self.use_key_check = QCheckBox(language_wrapper.language_word_dict.get("ssh_login_widget_button_use_key_auth"))
21
+ self.connect_btn = QPushButton(language_wrapper.language_word_dict.get("ssh_login_widget_button_connect"))
22
+ self.disconnect_btn = QPushButton(language_wrapper.language_word_dict.get("ssh_login_widget_button_disconnect"))
23
+ self.status_label = QLabel(language_wrapper.language_word_dict.get("ssh_login_widget_status_disconnected"))
24
+
25
+ # 初始化 UI
26
+ self._setup_ui()
27
+
28
+ def _setup_ui(self):
29
+ """建立登入介面 UI"""
30
+ self.host_edit.setPlaceholderText(language_wrapper.language_word_dict.get("ssh_login_widget_placeholder_host"))
31
+ self.port_spin.setRange(1, 65535)
32
+ self.port_spin.setValue(22)
33
+ self.user_edit.setPlaceholderText(
34
+ language_wrapper.language_word_dict.get("ssh_login_widget_placeholder_username"))
35
+ self.pass_edit.setPlaceholderText(
36
+ language_wrapper.language_word_dict.get("ssh_login_widget_placeholder_password"))
37
+ self.pass_edit.setEchoMode(QLineEdit.EchoMode.Password)
38
+ self.key_edit.setPlaceholderText(
39
+ language_wrapper.language_word_dict.get("ssh_login_widget_placeholder_private_key"))
40
+
41
+ # 佈局設計
42
+ top = QHBoxLayout()
43
+ top.addWidget(QLabel(language_wrapper.language_word_dict.get("ssh_login_widget_label_host")))
44
+ top.addWidget(self.host_edit)
45
+ top.addWidget(QLabel(language_wrapper.language_word_dict.get("ssh_login_widget_label_port")))
46
+ top.addWidget(self.port_spin)
47
+ top.addWidget(QLabel(language_wrapper.language_word_dict.get("ssh_login_widget_label_user")))
48
+ top.addWidget(self.user_edit)
49
+
50
+ auth = QHBoxLayout()
51
+ auth.addWidget(self.use_key_check)
52
+ auth.addWidget(QLabel(language_wrapper.language_word_dict.get("ssh_login_widget_label_key")))
53
+ auth.addWidget(self.key_edit)
54
+ auth.addWidget(QLabel(language_wrapper.language_word_dict.get("ssh_login_widget_label_password")))
55
+ auth.addWidget(self.pass_edit)
56
+
57
+ conn = QHBoxLayout()
58
+ conn.addWidget(self.connect_btn)
59
+ conn.addWidget(self.disconnect_btn)
60
+ conn.addWidget(self.status_label)
61
+
62
+ root = QVBoxLayout()
63
+ root.addLayout(top)
64
+ root.addLayout(auth)
65
+ root.addLayout(conn)
66
+
67
+ self.setLayout(root)
@@ -0,0 +1,49 @@
1
+ import sys
2
+
3
+ from PySide6.QtCore import Qt
4
+ from PySide6.QtWidgets import QWidget, QVBoxLayout, QSplitter, QApplication, QSizePolicy
5
+
6
+ from automation_ide.automation_editor_ui.connect_gui.ssh.ssh_command_widget import SSHCommandWidget
7
+ from automation_ide.automation_editor_ui.connect_gui.ssh.ssh_file_viewer_widget import SSHFileTreeManager
8
+ from automation_ide.automation_editor_ui.connect_gui.ssh.ssh_login_widget import LoginWidget
9
+
10
+
11
+ class SSHMainWidget(QWidget):
12
+ def __init__(self, parent=None):
13
+ super().__init__(parent)
14
+
15
+ # 已經實作好的元件
16
+ self.login_widget = LoginWidget()
17
+ self.login_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
18
+ self.file_tree = SSHFileTreeManager(external_login_widget=self.login_widget, add_login_widget=False)
19
+ self.command_widget = SSHCommandWidget(external_login_widget=self.login_widget, add_login_widget=False)
20
+
21
+ # 整體垂直布局
22
+ main_layout = QVBoxLayout(self)
23
+
24
+ # 上方放登入區
25
+ main_layout.addWidget(self.login_widget)
26
+
27
+ # Splitter 左右分割
28
+ splitter = QSplitter(Qt.Orientation.Horizontal)
29
+ splitter.addWidget(self.file_tree) # 左邊檔案瀏覽器
30
+ splitter.addWidget(self.command_widget) # 右邊命令區
31
+ # 設定初始比例:左邊 30%,右邊 70%
32
+ splitter.setSizes([300, 700]) # 數值會依視窗大小縮放
33
+ splitter.setStretchFactor(0, 1)
34
+ splitter.setStretchFactor(1, 2)
35
+
36
+ # 讓拖曳更平滑:加寬 handle 並避免完全收合
37
+ splitter.setHandleWidth(8)
38
+ splitter.setCollapsible(0, False)
39
+ splitter.setCollapsible(1, False)
40
+
41
+ main_layout.addWidget(splitter)
42
+ self.setLayout(main_layout)
43
+
44
+
45
+ if __name__ == "__main__":
46
+ app = QApplication(sys.argv)
47
+ win = SSHMainWidget()
48
+ win.showMaximized()
49
+ sys.exit(app.exec())