je-editor 0.0.222__py3-none-any.whl → 0.0.224__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.

Potentially problematic release.


This version of je-editor might be problematic. Click here for more details.

Files changed (75) hide show
  1. je_editor/__init__.py +2 -2
  2. je_editor/git_client/commit_graph.py +7 -7
  3. je_editor/git_client/git_action.py +0 -7
  4. je_editor/pyside_ui/browser/browser_view.py +16 -4
  5. je_editor/pyside_ui/browser/browser_widget.py +43 -29
  6. je_editor/pyside_ui/browser/main_browser_widget.py +85 -0
  7. je_editor/pyside_ui/code/auto_save/auto_save_manager.py +34 -2
  8. je_editor/pyside_ui/code/auto_save/auto_save_thread.py +19 -6
  9. je_editor/pyside_ui/code/code_format/pep8_format.py +53 -9
  10. je_editor/pyside_ui/code/code_process/code_exec.py +88 -52
  11. je_editor/pyside_ui/code/plaintext_code_edit/code_edit_plaintext.py +116 -55
  12. je_editor/pyside_ui/code/running_process_manager.py +19 -1
  13. je_editor/pyside_ui/code/shell_process/shell_exec.py +71 -48
  14. je_editor/pyside_ui/code/syntax/python_syntax.py +45 -10
  15. je_editor/pyside_ui/code/syntax/syntax_setting.py +40 -12
  16. je_editor/pyside_ui/code/textedit_code_result/code_record.py +34 -12
  17. je_editor/pyside_ui/code/variable_inspector/inspector_gui.py +53 -6
  18. je_editor/pyside_ui/dialog/ai_dialog/set_ai_dialog.py +30 -3
  19. je_editor/pyside_ui/dialog/file_dialog/create_file_dialog.py +35 -2
  20. je_editor/pyside_ui/dialog/file_dialog/open_file_dialog.py +33 -5
  21. je_editor/pyside_ui/dialog/file_dialog/save_file_dialog.py +25 -3
  22. je_editor/pyside_ui/dialog/search_ui/search_error_box.py +26 -1
  23. je_editor/pyside_ui/dialog/search_ui/search_text_box.py +26 -1
  24. je_editor/pyside_ui/git_ui/code_diff_compare/code_diff_viewer_widget.py +11 -11
  25. je_editor/pyside_ui/git_ui/git_client/commit_table.py +46 -8
  26. je_editor/pyside_ui/git_ui/git_client/git_branch_tree_widget.py +49 -15
  27. je_editor/pyside_ui/git_ui/git_client/git_client_gui.py +81 -16
  28. je_editor/pyside_ui/git_ui/git_client/graph_view.py +64 -20
  29. je_editor/pyside_ui/main_ui/ai_widget/ai_config.py +20 -5
  30. je_editor/pyside_ui/main_ui/ai_widget/ask_thread.py +20 -1
  31. je_editor/pyside_ui/main_ui/ai_widget/chat_ui.py +56 -41
  32. je_editor/pyside_ui/main_ui/ai_widget/langchain_interface.py +45 -6
  33. je_editor/pyside_ui/main_ui/console_widget/console_gui.py +44 -12
  34. je_editor/pyside_ui/main_ui/console_widget/qprocess_adapter.py +34 -13
  35. je_editor/pyside_ui/main_ui/dock/destroy_dock.py +33 -2
  36. je_editor/pyside_ui/main_ui/editor/editor_widget.py +104 -20
  37. je_editor/pyside_ui/main_ui/editor/editor_widget_dock.py +34 -7
  38. je_editor/pyside_ui/main_ui/editor/process_input.py +38 -11
  39. je_editor/pyside_ui/main_ui/ipython_widget/rich_jupyter.py +46 -11
  40. je_editor/pyside_ui/main_ui/main_editor.py +180 -42
  41. je_editor/pyside_ui/main_ui/menu/check_style_menu/build_check_style_menu.py +51 -28
  42. je_editor/pyside_ui/main_ui/menu/dock_menu/build_dock_menu.py +83 -36
  43. je_editor/pyside_ui/main_ui/menu/file_menu/build_file_menu.py +70 -17
  44. je_editor/pyside_ui/main_ui/menu/help_menu/build_help_menu.py +35 -4
  45. je_editor/pyside_ui/main_ui/menu/language_menu/build_language_server.py +41 -1
  46. je_editor/pyside_ui/main_ui/menu/python_env_menu/build_venv_menu.py +100 -42
  47. je_editor/pyside_ui/main_ui/menu/run_menu/build_run_menu.py +57 -7
  48. je_editor/pyside_ui/main_ui/menu/run_menu/under_run_menu/build_debug_menu.py +50 -4
  49. je_editor/pyside_ui/main_ui/menu/run_menu/under_run_menu/build_program_menu.py +52 -6
  50. je_editor/pyside_ui/main_ui/menu/run_menu/under_run_menu/build_shell_menu.py +44 -4
  51. je_editor/pyside_ui/main_ui/menu/run_menu/under_run_menu/utils.py +23 -1
  52. je_editor/pyside_ui/main_ui/menu/set_menu_bar.py +37 -12
  53. je_editor/pyside_ui/main_ui/menu/style_menu/build_style_menu.py +44 -7
  54. je_editor/pyside_ui/main_ui/menu/tab_menu/build_tab_menu.py +127 -44
  55. je_editor/pyside_ui/main_ui/menu/text_menu/build_text_menu.py +65 -1
  56. je_editor/pyside_ui/main_ui/save_settings/setting_utils.py +18 -1
  57. je_editor/pyside_ui/main_ui/save_settings/user_color_setting_file.py +33 -3
  58. je_editor/pyside_ui/main_ui/save_settings/user_setting_file.py +38 -11
  59. je_editor/pyside_ui/main_ui/system_tray/extend_system_tray.py +39 -2
  60. je_editor/start_editor.py +26 -1
  61. je_editor/utils/encodings/python_encodings.py +101 -98
  62. je_editor/utils/file/open/open_file.py +36 -19
  63. je_editor/utils/file/save/save_file.py +35 -14
  64. je_editor/utils/json/json_file.py +29 -14
  65. je_editor/utils/json_format/json_process.py +33 -2
  66. je_editor/utils/logging/loggin_instance.py +38 -8
  67. je_editor/utils/multi_language/multi_language_wrapper.py +29 -4
  68. je_editor/utils/redirect_manager/redirect_manager_class.py +49 -11
  69. je_editor/utils/venv_check/check_venv.py +45 -15
  70. {je_editor-0.0.222.dist-info → je_editor-0.0.224.dist-info}/METADATA +1 -1
  71. {je_editor-0.0.222.dist-info → je_editor-0.0.224.dist-info}/RECORD +74 -74
  72. je_editor/git_client/github.py +0 -81
  73. {je_editor-0.0.222.dist-info → je_editor-0.0.224.dist-info}/WHEEL +0 -0
  74. {je_editor-0.0.222.dist-info → je_editor-0.0.224.dist-info}/licenses/LICENSE +0 -0
  75. {je_editor-0.0.222.dist-info → je_editor-0.0.224.dist-info}/top_level.txt +0 -0
@@ -3,25 +3,63 @@ from PySide6.QtWidgets import QTableView
3
3
 
4
4
 
5
5
  class CommitTable(QTableView):
6
+ """
7
+ CommitTable 類別:用來顯示 Git Commit 紀錄的表格
8
+ CommitTable class: A table view to display Git commit history
9
+ """
10
+
6
11
  def __init__(self, parent=None):
7
12
  super().__init__(parent)
8
- self.model_data = QStandardItemModel(0, 5, self) # 多一欄
13
+
14
+ # 建立一個 QStandardItemModel,初始為 0 行 5 欄
15
+ # Create a QStandardItemModel with 0 rows and 5 columns
16
+ self.model_data = QStandardItemModel(0, 5, self) # 多一欄 / 5 columns
17
+
18
+ # 設定表頭標籤
19
+ # Set horizontal header labels
9
20
  self.model_data.setHorizontalHeaderLabels(["#", "SHA", "Message", "Author", "Date"])
21
+
22
+ # 將模型綁定到 QTableView
23
+ # Set the model for QTableView
10
24
  self.setModel(self.model_data)
25
+
26
+ # 設定選擇行為為整列選取
27
+ # Set selection behavior to select entire rows
11
28
  self.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows)
29
+
30
+ # 禁止編輯表格內容
31
+ # Disable editing of table cells
12
32
  self.setEditTriggers(QTableView.EditTrigger.NoEditTriggers)
33
+
34
+ # 讓最後一欄自動延展填滿
35
+ # Stretch the last column to fill available space
13
36
  self.horizontalHeader().setStretchLastSection(True)
14
37
 
15
38
  def set_commits(self, commits):
39
+ """
40
+ 將 commit 資料填入表格
41
+ Populate the table with commit data
42
+ :param commits: commit 資料清單 (list of dicts with sha, message, author, date)
43
+ """
44
+ # 清空現有資料
45
+ # Clear existing rows
16
46
  self.model_data.setRowCount(0)
17
- for idx, c in enumerate(commits, start=1):
47
+
48
+ # 遍歷 commit 清單,逐行加入表格
49
+ # Iterate over commits and append rows
50
+ for index, commit in enumerate(commits, start=1):
18
51
  row = [
19
- QStandardItem(str(idx)), # 行號
20
- QStandardItem(c["sha"][:7]),
21
- QStandardItem(c["message"]),
22
- QStandardItem(c["author"]),
23
- QStandardItem(c["date"]),
52
+ QStandardItem(str(index)), # 行號 / row number
53
+ QStandardItem(commit["sha"][:7]), # SHA 短碼 / short SHA
54
+ QStandardItem(commit["message"]), # 提交訊息 / commit message
55
+ QStandardItem(commit["author"]), # 作者 / author
56
+ QStandardItem(commit["date"]), # 日期 / date
24
57
  ]
58
+ # 設定每個欄位不可編輯
59
+ # Make each item non-editable
25
60
  for item in row:
26
61
  item.setEditable(False)
27
- self.model_data.appendRow(row)
62
+
63
+ # 將這一列加入模型
64
+ # Append row to the model
65
+ self.model_data.appendRow(row)
@@ -19,64 +19,89 @@ log = logging.getLogger(__name__)
19
19
 
20
20
 
21
21
  class GitTreeViewGUI(QWidget):
22
+ """
23
+ GitTreeViewGUI: Git 提交歷史圖形化檢視器
24
+ GitTreeViewGUI: A graphical viewer for Git commit history
25
+ """
26
+
22
27
  def __init__(self):
23
28
  super().__init__()
24
29
  self.language_wrapper_get = language_wrapper.language_word_dict.get
25
30
  self.setWindowTitle(self.language_wrapper_get("git_graph_title"))
26
31
 
32
+ # === 主版面配置 / Main layout ===
27
33
  git_treeview_main_layout = QVBoxLayout(self)
28
34
  git_treeview_main_layout.setContentsMargins(0, 0, 0, 0)
29
35
  git_treeview_main_layout.setSpacing(0)
30
36
 
37
+ # === 工具列 / Toolbar ===
31
38
  git_treeview_toolbar = QToolBar()
39
+
40
+ # 開啟 Git 專案動作 / Action to open Git repo
32
41
  open_repo_action = QAction(self.language_wrapper_get("git_graph_toolbar_open"), self)
33
42
  open_repo_action.triggered.connect(self.open_repo)
34
43
  git_treeview_toolbar.addAction(open_repo_action)
35
44
 
45
+ # 刷新圖表動作 / Action to refresh graph
36
46
  act_refresh = QAction(self.language_wrapper_get("git_graph_toolbar_refresh"), self)
37
47
  act_refresh.triggered.connect(self.refresh_graph)
38
48
  git_treeview_toolbar.addAction(act_refresh)
39
49
 
40
50
  git_treeview_main_layout.addWidget(git_treeview_toolbar)
41
51
 
52
+ # === 分割器 (左:圖形檢視 / 右:提交表格) ===
42
53
  splitter = QSplitter(Qt.Orientation.Horizontal)
43
- self.graph_view = CommitGraphView()
44
- self.commit_table = CommitTable()
54
+ self.graph_view = CommitGraphView() # 提交圖形檢視器 / commit graph view
55
+ self.commit_table = CommitTable() # 提交表格 / commit table
45
56
  splitter.addWidget(self.graph_view)
46
57
  splitter.addWidget(self.commit_table)
47
- splitter.setSizes([600, 400])
48
- git_treeview_main_layout.addWidget(splitter, 1) # 1 表示可伸縮
58
+ splitter.setSizes([600, 400]) # 左右比例 / left-right ratio
59
+ git_treeview_main_layout.addWidget(splitter, 1) # stretch=1 表示可伸縮
49
60
 
61
+ # === 狀態列 / Status bar ===
50
62
  self.status = QStatusBar()
51
63
  git_treeview_main_layout.addWidget(self.status)
52
64
 
65
+ # === Git 相關屬性 ===
53
66
  self.repo_path = None
54
67
  self.git = None
55
68
 
69
+ # === 檔案監控器 / File system watcher ===
56
70
  self.watcher = QFileSystemWatcher()
57
71
  self.watcher.directoryChanged.connect(self._on_git_changed)
58
72
  self.watcher.fileChanged.connect(self._on_git_changed)
59
73
 
74
+ # === 定時器 (延遲刷新) / Timer for delayed refresh ===
60
75
  self.refresh_timer = QTimer()
61
76
  self.refresh_timer.setSingleShot(True)
62
77
  self.refresh_timer.timeout.connect(self.refresh_graph)
63
78
 
79
+ # === 表格選擇事件 / Table selection event ===
64
80
  self.commit_table.selectionModel().selectionChanged.connect(self._on_table_selection)
65
81
 
66
82
  def _on_table_selection(self, selected, _):
83
+ """
84
+ 當使用者在提交表格中選擇某一列時,讓圖形檢視器聚焦到該行
85
+ When user selects a row in commit table, focus graph view on that row
86
+ """
67
87
  if not selected.indexes():
68
88
  return
69
89
  row = selected.indexes()[0].row()
70
90
  self.graph_view.focus_row(row)
71
91
 
72
92
  def open_repo(self):
73
- # Open repo and set up the graph.
93
+ """
94
+ 開啟 Git 專案並初始化圖形檢視
95
+ Open a Git repository and set up the graph view
96
+ """
74
97
  path = QFileDialog.getExistingDirectory(self, self.language_wrapper_get("git_graph_menu_open_repo"))
75
98
  if not path:
76
99
  return
77
100
  repo_path = Path(path)
78
101
  git = GitCLI(repo_path)
79
102
  if not git.is_git_repo():
103
+ # 若不是 Git 專案,顯示警告
104
+ # Show warning if not a Git repo
80
105
  QMessageBox.warning(self, self.language_wrapper_get("git_graph_title"),
81
106
  self.language_wrapper_get("git_graph_error_not_git"))
82
107
  return
@@ -86,11 +111,15 @@ class GitTreeViewGUI(QWidget):
86
111
  self.refresh_graph()
87
112
 
88
113
  def _setup_watcher(self):
114
+ """
115
+ 設定檔案監控,監控 .git 目錄與相關檔案
116
+ Setup file watcher to monitor .git directory and related files
117
+ """
89
118
  self.watcher.removePaths(self.watcher.files())
90
119
  self.watcher.removePaths(self.watcher.directories())
91
120
  if not self.repo_path:
92
121
  return
93
- git_dir = self.repo_path / ".git_client"
122
+ git_dir = self.repo_path / ".git_client" # ⚠️ 這裡應該是 ".git",可能是自訂路徑
94
123
  if git_dir.exists():
95
124
  self.watcher.addPath(str(git_dir))
96
125
  for f in ["HEAD", "packed-refs"]:
@@ -102,21 +131,26 @@ class GitTreeViewGUI(QWidget):
102
131
  self.watcher.addPath(str(refs_dir))
103
132
 
104
133
  def _on_git_changed(self):
105
- # Refresh git tree timer
134
+ """
135
+ 當 Git 目錄變更時,啟動延遲刷新
136
+ When Git directory changes, start delayed refresh
137
+ """
106
138
  self.refresh_timer.start(500)
107
139
 
108
140
  def refresh_graph(self):
109
- # Refresh git tree
141
+ """
142
+ 刷新 Git 提交圖與提交表格
143
+ Refresh Git commit graph and commit table
144
+ """
110
145
  if not self.git:
111
146
  return
112
147
  try:
113
- refs = self.git.get_all_refs()
114
- commits = self.git.get_commits(max_count=500)
148
+ refs = self.git.get_all_refs() # 取得所有 refs
149
+ commits = self.git.get_commits(max_count=500) # 取得最近 500 筆提交
115
150
  graph = CommitGraph()
116
- graph.build(commits, refs)
117
- self.graph_view.set_graph(graph)
118
- self.commit_table.set_commits(commits)
151
+ graph.build(commits, refs) # 建立提交圖
152
+ self.graph_view.set_graph(graph) # 更新圖形檢視
153
+ self.commit_table.set_commits(commits) # 更新提交表格
119
154
  except Exception as e:
120
155
  QMessageBox.critical(self, self.language_wrapper_get("git_graph_title"),
121
- f"{self.language_wrapper_get('git_graph_error_exec_failed')}\n{e}")
122
-
156
+ f"{self.language_wrapper_get('git_graph_error_exec_failed')}\n{e}")
@@ -1,6 +1,6 @@
1
1
  from pathlib import Path
2
2
 
3
- from PySide6.QtCore import Qt
3
+ from PySide6.QtCore import Qt, QTimer
4
4
  from PySide6.QtGui import QTextOption, QTextCharFormat, QColor, QFont, QSyntaxHighlighter, QAction
5
5
  from PySide6.QtWidgets import (
6
6
  QWidget, QVBoxLayout, QHBoxLayout, QLabel,
@@ -15,9 +15,9 @@ class GitChangeItem:
15
15
  簡單的資料結構,用來存放檔案變更資訊。
16
16
  """
17
17
 
18
- def __init__(self, file_path: str, change_status: str):
19
- self.file_path = file_path # repo 相對路徑 / repo-relative path
20
- self.change_status = change_status # 狀態,例如 'untracked', 'modified', 'deleted', 'renamed', 'staged'
18
+ def __init__(self, path: str, status: str):
19
+ self.path = path # repo 相對路徑 / repo-relative path
20
+ self.status = status # 狀態,例如 'untracked', 'modified', 'deleted', 'renamed', 'staged'
21
21
 
22
22
 
23
23
  class GitGui(QWidget):
@@ -36,6 +36,7 @@ class GitGui(QWidget):
36
36
  self.checkout_button = QPushButton("Checkout")
37
37
  self.clone_repo_button = QPushButton("Clone Repo")
38
38
  self.repo_status_label = QLabel("Status: -")
39
+ self.commit_status_label = QLabel("Unpushed commits: ...")
39
40
 
40
41
  top = QHBoxLayout()
41
42
  top.addWidget(self.repo_path_label, 1)
@@ -48,7 +49,6 @@ class GitGui(QWidget):
48
49
  # === Left: changes list / 左側:變更清單 ===
49
50
  self.changes_list_widget = QListWidget()
50
51
  self.changes_list_widget.setSelectionMode(QListWidget.SelectionMode.SingleSelection)
51
- self.changes_list_widget.setMinimumWidth(420)
52
52
 
53
53
  # === Right: diff / info viewer / 右側:差異或資訊檢視器 ===
54
54
  self.diff_viewer = QPlainTextEdit()
@@ -84,6 +84,7 @@ class GitGui(QWidget):
84
84
  self.commit_button = QPushButton("Commit")
85
85
  self.unstage_all_button = QPushButton("Unstage All")
86
86
  self.track_all_untracked_button = QPushButton("Track All Untracked")
87
+ self.git_push_button = QPushButton("Push")
87
88
 
88
89
  bottom = QHBoxLayout()
89
90
  bottom.addWidget(QLabel("Message:"))
@@ -94,11 +95,13 @@ class GitGui(QWidget):
94
95
  bottom.addWidget(self.stage_all_button)
95
96
  bottom.addWidget(self.commit_button)
96
97
  bottom.addWidget(self.track_all_untracked_button)
98
+ bottom.addWidget(self.git_push_button)
97
99
 
98
100
  # === Main layout / 主版面配置 ===
99
101
  center_layout = QVBoxLayout()
100
102
  center_layout.addLayout(top)
101
103
  center_layout.addWidget(self.repo_status_label)
104
+ center_layout.addWidget(self.commit_status_label)
102
105
  center_layout.addWidget(splitter, 1)
103
106
  center_layout.addLayout(bottom)
104
107
  self.setLayout(center_layout)
@@ -115,6 +118,7 @@ class GitGui(QWidget):
115
118
  self.commit_button.clicked.connect(self.on_commit_staged_changes)
116
119
  self.unstage_all_button.clicked.connect(self.on_unstage_all_changes)
117
120
  self.track_all_untracked_button.clicked.connect(self.on_track_all_untracked_files)
121
+ self.git_push_button.clicked.connect(self.on_push_to_github)
118
122
 
119
123
  self._update_ui_controls(enabled=False)
120
124
 
@@ -133,6 +137,12 @@ class GitGui(QWidget):
133
137
 
134
138
  center_layout.setMenuBar(menubar)
135
139
 
140
+ # === Timer ===
141
+ self.update_commit_status_timer = QTimer()
142
+ self.update_commit_status_timer.setInterval(1000)
143
+ self.update_commit_status_timer.timeout.connect(self.update_commit_status)
144
+ self.update_commit_status_timer.start()
145
+
136
146
  # ---------- Repo operations ----------
137
147
  # ---------- 儲存庫操作 ----------
138
148
 
@@ -258,16 +268,16 @@ class GitGui(QWidget):
258
268
 
259
269
  # Normalize paths (for rename "a -> b" 取目的與來源)
260
270
  src, dst = None, None
261
- if "->" in change.file_path and change.change_status in ("renamed", "staged"):
262
- parts = [p.strip() for p in change.file_path.split("->")]
271
+ if "->" in change.path and change.status in ("renamed", "staged"):
272
+ parts = [p.strip() for p in change.path.split("->")]
263
273
  if len(parts) == 2:
264
274
  src, dst = parts
265
- rel = dst or change.file_path
275
+ rel = dst or change.path
266
276
  abs_path = Path(current_repo.working_tree_dir) / Path(rel)
267
277
 
268
278
  try:
269
279
  # Untracked: 顯示新增檔案的內容(模擬 unified diff)
270
- if change.change_status == "untracked":
280
+ if change.status == "untracked":
271
281
  if not abs_path.exists():
272
282
  self._safe_set_diff_text(f"(untracked file missing: {rel})")
273
283
  return
@@ -282,7 +292,7 @@ class GitGui(QWidget):
282
292
  return
283
293
 
284
294
  # Deleted: 顯示與 HEAD/Index 的差異(若工作樹檔案不存在也能呈現)
285
- if change.change_status == "deleted":
295
+ if change.status == "deleted":
286
296
  # working tree vs index(未暫存刪除)
287
297
  if rel and not current_repo.index.diff(None, paths=[rel]):
288
298
  # 若 diff(None) 空,試試 index vs HEAD
@@ -296,7 +306,7 @@ class GitGui(QWidget):
296
306
  return
297
307
 
298
308
  # Renamed: 顯示 rename 變更;若僅 rename 無內容改動,diff 可能為空
299
- if change.change_status == "renamed":
309
+ if change.status == "renamed":
300
310
  # 優先顯示 staged rename
301
311
  diff_text = ""
302
312
  try:
@@ -319,7 +329,7 @@ class GitGui(QWidget):
319
329
  return
320
330
 
321
331
  # Staged vs HEAD
322
- if change.change_status == "staged":
332
+ if change.status == "staged":
323
333
  diff_text = current_repo.git.diff("--cached", rel)
324
334
  if not diff_text.strip():
325
335
  self._safe_set_diff_text("(no staged changes vs HEAD)")
@@ -328,7 +338,7 @@ class GitGui(QWidget):
328
338
  return
329
339
 
330
340
  # Modified / general unstaged
331
- if change.change_status in ("modified",):
341
+ if change.status in ("modified",):
332
342
  # working tree vs index
333
343
  diff_text = current_repo.git.diff(rel)
334
344
  if not diff_text.strip():
@@ -344,7 +354,7 @@ class GitGui(QWidget):
344
354
  return
345
355
 
346
356
  # Fallback
347
- self._safe_set_diff_text(f"(no diff handler for status: {change.change_status})")
357
+ self._safe_set_diff_text(f"(no diff handler for status: {change.status})")
348
358
 
349
359
  except GitCommandError as e:
350
360
  self._safe_set_diff_text(f"Git error while generating diff:\n{e}")
@@ -433,7 +443,7 @@ class GitGui(QWidget):
433
443
  Add a file change entry to the list.
434
444
  將檔案變更項目加入清單。
435
445
  """
436
- item_text = f"[{change.change_status}] {change.file_path}"
446
+ item_text = f"[{change.status}] {change.path}"
437
447
  list_item = QListWidgetItem(item_text)
438
448
  list_item.setData(Qt.ItemDataRole.UserRole, change) # 存放 GitChangeItem 物件
439
449
  list_item.setCheckState(Qt.CheckState.Unchecked) # 預設未勾選
@@ -513,6 +523,7 @@ class GitGui(QWidget):
513
523
  try:
514
524
  file_paths = []
515
525
  for change_entry in selected_changes:
526
+ print(change_entry.path)
516
527
  if "->" in change_entry.path and change_entry.status in ("renamed", "staged"):
517
528
  parts = change_entry.path.split("->")
518
529
  source_path = parts[0].strip()
@@ -582,7 +593,7 @@ class GitGui(QWidget):
582
593
  self.branch_selector, self.checkout_button, self.changes_list_widget,
583
594
  self.diff_viewer, self.commit_message_input, self.stage_selected_button,
584
595
  self.unstage_selected_button, self.stage_all_button, self.commit_button,
585
- self.unstage_all_button, self.track_all_untracked_button
596
+ self.unstage_all_button, self.track_all_untracked_button, self.git_push_button
586
597
  ):
587
598
  widget.setEnabled(enabled)
588
599
 
@@ -655,6 +666,60 @@ class GitGui(QWidget):
655
666
  except GitCommandError as e:
656
667
  QMessageBox.critical(self, "Clone Error", str(e))
657
668
 
669
+ # ===== GitHub =====
670
+
671
+ def on_push_to_github(self):
672
+ if not self.current_repo:
673
+ QMessageBox.warning(self, "Warning", "No repository opened.")
674
+ return
675
+ try:
676
+ origin = self.current_repo.remote(name="origin")
677
+ result = origin.push()
678
+ msg = "\n".join(str(r) for r in result)
679
+ QMessageBox.information(self, "Push Result", f"Pushed to origin:\n{msg}")
680
+ except Exception as e:
681
+ QMessageBox.critical(self, "Track Untracked Error", str(e))
682
+
683
+ def get_unpushed_commit_count(self, remote_name: str = "origin") -> dict:
684
+ try:
685
+ repo = self.current_repo
686
+ if repo is None:
687
+ return {"ahead": 0, "behind": 0, "error": "No repo loaded"}
688
+ if repo.bare:
689
+ return {"ahead": 0, "behind": 0, "error": "Repository is bare"}
690
+ if repo.head.is_detached:
691
+ return {"ahead": 0, "behind": 0, "error": "HEAD is detached"}
692
+
693
+ branch = repo.active_branch
694
+ remote = repo.remote(remote_name)
695
+ remote.fetch()
696
+
697
+ upstream_ref = f"{remote_name}/{branch.name}"
698
+ if upstream_ref not in repo.refs:
699
+ return {"ahead": 0, "behind": 0, "error": f"No upstream branch for {branch.name}"}
700
+
701
+ ahead_commits = list(repo.iter_commits(f"{upstream_ref}..{branch.name}"))
702
+ behind_commits = list(repo.iter_commits(f"{branch.name}..{upstream_ref}"))
703
+
704
+ return {"ahead": len(ahead_commits), "behind": len(behind_commits), "error": None}
705
+
706
+ except GitCommandError as e:
707
+ return {"ahead": 0, "behind": 0, "error": f"Git error: {e}"}
708
+ except Exception as e:
709
+ return {"ahead": 0, "behind": 0, "error": str(e)}
710
+
711
+ def update_commit_status(self):
712
+ result = self.get_unpushed_commit_count()
713
+ if result["error"]:
714
+ self.commit_status_label.setText(f"Error: {result['error']}")
715
+ else:
716
+ self.commit_status_label.setText(
717
+ f"Ahead (push): {result['ahead']} | Behind (pull): {result['behind']}"
718
+ )
719
+
720
+
721
+ # ===== Theme =====
722
+
658
723
  def apply_light_theme(self):
659
724
  """
660
725
  Switch to light theme highlighting.
@@ -14,18 +14,18 @@ from PySide6.QtWidgets import (
14
14
  from je_editor.git_client.commit_graph import CommitGraph
15
15
  from je_editor.utils.multi_language.multi_language_wrapper import language_wrapper
16
16
 
17
- # Layout constants
18
- NODE_RADIUS = 6
19
- ROW_HEIGHT = 28
20
- LANE_WIDTH = 22
21
- LEFT_MARGIN = 220 # space for SHA/message text area on the left
22
-
23
- # Colors
24
- EDGE_COLOR = QColor("#888")
25
- TEXT_COLOR = QColor("#222")
26
- BG_COLOR = QColor("#ffffff")
27
-
28
- # A small, repeatable lane palette
17
+ # === 版面配置常數 / Layout constants ===
18
+ NODE_RADIUS = 6 # 節點半徑 / node radius
19
+ ROW_HEIGHT = 28 # 每一列高度 / row height
20
+ LANE_WIDTH = 22 # 每一條 lane 的寬度 / lane width
21
+ LEFT_MARGIN = 220 # 左側 SHA/訊息文字區域保留空間 / left margin for SHA/message text
22
+
23
+ # === 顏色設定 / Colors ===
24
+ EDGE_COLOR = QColor("#888") # 邊線顏色 / edge color
25
+ TEXT_COLOR = QColor("#222") # 文字顏色 / text color
26
+ BG_COLOR = QColor("#ffffff") # 背景顏色 / background color
27
+
28
+ # Lane 顏色調色盤 / Lane color palette
29
29
  LANE_COLORS = [
30
30
  QColor("#4C78A8"),
31
31
  QColor("#F58518"),
@@ -41,24 +41,38 @@ LANE_COLORS = [
41
41
 
42
42
 
43
43
  def lane_color(lane: int) -> QColor:
44
+ """
45
+ 根據 lane index 回傳顏色
46
+ Return color based on lane index
47
+ """
44
48
  if lane < 0:
45
49
  return QColor("#999")
46
50
  return LANE_COLORS[lane % len(LANE_COLORS)]
47
51
 
48
52
 
49
53
  class CommitGraphView(QGraphicsView):
54
+ """
55
+ Git Commit Graph 視覺化檢視器
56
+ Git commit graph visualization view
57
+ """
58
+
50
59
  def __init__(self, parent=None):
51
60
  super().__init__(parent)
52
61
  self.language_wrapper_get = language_wrapper.language_word_dict.get
62
+
63
+ # 設定背景與抗鋸齒 / set background and antialiasing
53
64
  self.setBackgroundBrush(QBrush(BG_COLOR))
54
65
  self.setRenderHints(
55
66
  self.renderHints()
56
67
  | QPainter.RenderHint.Antialiasing
57
68
  | QPainter.RenderHint.TextAntialiasing
58
69
  )
70
+
71
+ # 啟用拖曳與縮放模式 / enable drag and zoom mode
59
72
  self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
60
73
  self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
61
74
 
75
+ # 建立場景 / create scene
62
76
  self._scene = QGraphicsScene(self)
63
77
  self.setScene(self._scene)
64
78
 
@@ -67,21 +81,32 @@ class CommitGraphView(QGraphicsView):
67
81
  self._zoom = 1.0
68
82
 
69
83
  def set_graph(self, graph: CommitGraph):
84
+ """
85
+ 設定 commit graph 並重新繪製
86
+ Set commit graph and redraw
87
+ """
70
88
  self.graph = graph
71
89
  self._redraw()
72
90
 
73
91
  def _lane_x(self, lane: int) -> float:
92
+ # 計算 lane 的 X 座標 / calculate X position for lane
74
93
  return lane * LANE_WIDTH + NODE_RADIUS * 2
75
94
 
76
95
  def _row_y(self, row: int) -> float:
96
+ # 計算 row 的 Y 座標 / calculate Y position for row
77
97
  return row * ROW_HEIGHT + NODE_RADIUS * 2
78
98
 
79
99
  def _redraw(self):
100
+ """
101
+ 重新繪製整個 commit graph
102
+ Redraw the entire commit graph
103
+ """
80
104
  self._scene.clear()
81
105
  if not self.graph or not self.graph.nodes:
82
106
  self._scene.setSceneRect(QRectF(0, 0, 800, 400))
83
107
  return
84
108
 
109
+ # === 繪製邊線 (父子關係) / Draw edges (parent-child relationships) ===
85
110
  edge_pen = QPen(EDGE_COLOR, 2)
86
111
  for row, node in enumerate(self.graph.nodes):
87
112
  if not node.parent_shas:
@@ -95,6 +120,8 @@ class CommitGraphView(QGraphicsView):
95
120
  parent_node = self.graph.nodes[prow]
96
121
  x1 = self._lane_x(parent_node.lane_index)
97
122
  y1 = self._row_y(prow)
123
+
124
+ # 使用三次貝茲曲線繪製邊線 / cubic Bezier curve for edge
98
125
  path = QPainterPath(QPointF(x0, y0))
99
126
  ctrl_y = (y0 + y1) / 2.0
100
127
  path.cubicTo(QPointF(x0, ctrl_y), QPointF(x1, ctrl_y), QPointF(x1, y1))
@@ -102,6 +129,7 @@ class CommitGraphView(QGraphicsView):
102
129
  edge_item.setPen(edge_pen)
103
130
  self._scene.addItem(edge_item)
104
131
 
132
+ # === 繪製節點 (commit 圓點) / Draw nodes (commit circles) ===
105
133
  for row, node in enumerate(self.graph.nodes):
106
134
  cx = self._lane_x(node.lane_index)
107
135
  cy = self._row_y(row)
@@ -111,6 +139,7 @@ class CommitGraphView(QGraphicsView):
111
139
  )
112
140
  circle.setBrush(QBrush(lane_color(node.lane_index)))
113
141
  circle.setPen(QPen(Qt.PenStyle.NoPen))
142
+ # 設定 tooltip 顯示 commit 資訊 / tooltip with commit info
114
143
  circle.setToolTip(self.language_wrapper_get(
115
144
  "git_graph_tooltip_commit"
116
145
  ).format(short=node.commit_sha[:7],
@@ -119,11 +148,13 @@ class CommitGraphView(QGraphicsView):
119
148
  msg=node.commit_message))
120
149
  self._scene.addItem(circle)
121
150
 
151
+ # 在左側顯示行號 / show row number on the left
122
152
  label_item = QGraphicsSimpleTextItem(str(row + 1))
123
153
  label_item.setBrush(QBrush(TEXT_COLOR))
124
154
  label_item.setPos(-30, cy - NODE_RADIUS * 2)
125
155
  self._scene.addItem(label_item)
126
156
 
157
+ # 設定場景大小 / set scene rect
127
158
  self._scene.setSceneRect(
128
159
  QRectF(-40, 0,
129
160
  self._lane_x(max(n.lane_index for n in self.graph.nodes) + 1) + self._padding,
@@ -131,15 +162,19 @@ class CommitGraphView(QGraphicsView):
131
162
  )
132
163
 
133
164
  def _apply_initial_view(self):
134
- # Start with a mild zoom to fit rows comfortably
165
+ """
166
+ 初始縮放設定
167
+ Apply initial zoom setting
168
+ """
135
169
  self._zoom = 0.9
136
170
  self._apply_zoom_transform()
137
171
 
138
- # Zoom and interaction
172
+ # === 滑鼠滾輪縮放 / Mouse wheel zoom ===
139
173
  def wheelEvent(self, event):
140
174
  if not self._scene.items():
141
175
  return
142
176
 
177
+ # Ctrl+滾輪縮放,否則正常滾動
143
178
  # Ctrl+Wheel to zoom, else scroll normally
144
179
  if event.modifiers() & Qt.KeyboardModifier.ControlModifier:
145
180
  angle = event.angleDelta().y()
@@ -151,24 +186,33 @@ class CommitGraphView(QGraphicsView):
151
186
  super().wheelEvent(event)
152
187
 
153
188
  def _apply_zoom_transform(self):
189
+ """
190
+ 套用縮放轉換
191
+ Apply zoom transform
192
+ """
154
193
  t = QTransform()
155
194
  t.scale(self._zoom, self._zoom)
156
195
  self.setTransform(t)
157
196
 
158
197
  def resizeEvent(self, event):
198
+ """
199
+ 視窗大小改變時的事件
200
+ Handle resize event (keep scene rect, no auto-fit)
201
+ """
159
202
  super().resizeEvent(event)
160
- # Keep scene rect; do not auto-fit. Users control zoom.
161
- # Nothing else needed here.
203
+ # 保持場景大小,不自動縮放
204
+ # Keep scene rect; do not auto-fit
162
205
 
163
- # Optional helpers for external controllers
206
+ # === 外部控制輔助方法 / Helper for external controllers ===
164
207
  def focus_row(self, row: int):
165
208
  """
166
- Center the view around a specific row.
209
+ 將檢視器聚焦到指定的 row
210
+ Center the view around a specific row
167
211
  """
168
212
  if not self.graph or row < 0 or row >= len(self.graph.nodes):
169
213
  return
170
214
  y = self._row_y(row)
171
215
  rect = QRectF(0, y - ROW_HEIGHT * 2, self._scene.width(), ROW_HEIGHT * 4)
172
216
  self.fitInView(rect, Qt.AspectRatioMode.KeepAspectRatio)
173
- # Restore user zoom preference after focusing
174
- self._apply_zoom_transform()
217
+ # 聚焦後恢復使用者縮放比例 / restore user zoom after focusing
218
+ self._apply_zoom_transform()