Compare commits
No commits in common. "cff5f864e46435561b2f2f96adb4616bed6bbe91" and "01963ed6a72ad26d209deee93cc77a2ec00a96f3" have entirely different histories.
cff5f864e4
...
01963ed6a7
7 changed files with 58 additions and 323 deletions
|
|
@ -1,8 +1,6 @@
|
||||||
# 0.4.1
|
# 0.4.1
|
||||||
|
|
||||||
* Allow time log entries to be edited directly in their table cells
|
* Allow time log entries to be edited directly in their table cells
|
||||||
* Miscellaneous bug fixes for editing (list cursor positions/text selectivity, alarm removing newline)
|
|
||||||
* Add 'Close tab' nav item and shortcut
|
|
||||||
|
|
||||||
# 0.4
|
# 0.4
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@
|
||||||
"behaviour": "Behaviour",
|
"behaviour": "Behaviour",
|
||||||
"never": "Never",
|
"never": "Never",
|
||||||
"browse": "Browse",
|
"browse": "Browse",
|
||||||
"close_tab": "Close tab",
|
|
||||||
"previous": "Previous",
|
"previous": "Previous",
|
||||||
"previous_day": "Previous day",
|
"previous_day": "Previous day",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
|
|
|
||||||
|
|
@ -260,13 +260,6 @@ class MainWindow(QMainWindow):
|
||||||
nav_menu.addAction(act_today)
|
nav_menu.addAction(act_today)
|
||||||
self.addAction(act_today)
|
self.addAction(act_today)
|
||||||
|
|
||||||
act_close_tab = QAction(strings._("close_tab"), self)
|
|
||||||
act_close_tab.setShortcut("Ctrl+W")
|
|
||||||
act_close_tab.setShortcutContext(Qt.ApplicationShortcut)
|
|
||||||
act_close_tab.triggered.connect(self._close_current_tab)
|
|
||||||
nav_menu.addAction(act_close_tab)
|
|
||||||
self.addAction(act_close_tab)
|
|
||||||
|
|
||||||
act_find = QAction(strings._("find_on_page"), self)
|
act_find = QAction(strings._("find_on_page"), self)
|
||||||
act_find.setShortcut(QKeySequence.Find)
|
act_find.setShortcut(QKeySequence.Find)
|
||||||
act_find.triggered.connect(self.findBar.show_bar)
|
act_find.triggered.connect(self.findBar.show_bar)
|
||||||
|
|
@ -527,12 +520,6 @@ class MainWindow(QMainWindow):
|
||||||
|
|
||||||
self.tab_widget.removeTab(index)
|
self.tab_widget.removeTab(index)
|
||||||
|
|
||||||
def _close_current_tab(self):
|
|
||||||
"""Close the currently active tab via shortcuts (Ctrl+W)."""
|
|
||||||
idx = self.tab_widget.currentIndex()
|
|
||||||
if idx >= 0:
|
|
||||||
self._close_tab(idx)
|
|
||||||
|
|
||||||
def _on_tab_changed(self, index: int):
|
def _on_tab_changed(self, index: int):
|
||||||
"""Handle tab change - reconnect toolbar and sync UI."""
|
"""Handle tab change - reconnect toolbar and sync UI."""
|
||||||
if index < 0:
|
if index < 0:
|
||||||
|
|
|
||||||
|
|
@ -406,14 +406,9 @@ class MarkdownEditor(QTextEdit):
|
||||||
# Append the new marker
|
# Append the new marker
|
||||||
new_line = f"{new_line} ⏰ {time_str}"
|
new_line = f"{new_line} ⏰ {time_str}"
|
||||||
|
|
||||||
# --- : only replace the block's text, not its newline ---
|
bc = QTextCursor(block)
|
||||||
block_start = block.position()
|
|
||||||
block_end = block_start + len(line)
|
|
||||||
|
|
||||||
bc = QTextCursor(self.document())
|
|
||||||
bc.beginEditBlock()
|
bc.beginEditBlock()
|
||||||
bc.setPosition(block_start)
|
bc.select(QTextCursor.SelectionType.BlockUnderCursor)
|
||||||
bc.setPosition(block_end, QTextCursor.KeepAnchor)
|
|
||||||
bc.insertText(new_line)
|
bc.insertText(new_line)
|
||||||
bc.endEditBlock()
|
bc.endEditBlock()
|
||||||
|
|
||||||
|
|
@ -431,35 +426,6 @@ class MarkdownEditor(QTextEdit):
|
||||||
"""Public wrapper used by MainWindow for reminders."""
|
"""Public wrapper used by MainWindow for reminders."""
|
||||||
return self._get_current_line()
|
return self._get_current_line()
|
||||||
|
|
||||||
def _list_prefix_length_for_block(self, block) -> int:
|
|
||||||
"""Return the length (in chars) of the visual list prefix for the given
|
|
||||||
block (including leading indentation), or 0 if it's not a list item.
|
|
||||||
"""
|
|
||||||
line = block.text()
|
|
||||||
stripped = line.lstrip()
|
|
||||||
leading_spaces = len(line) - len(stripped)
|
|
||||||
|
|
||||||
# Checkbox (Unicode display)
|
|
||||||
if stripped.startswith(
|
|
||||||
f"{self._CHECK_UNCHECKED_DISPLAY} "
|
|
||||||
) or stripped.startswith(f"{self._CHECK_CHECKED_DISPLAY} "):
|
|
||||||
return leading_spaces + 2 # icon + space
|
|
||||||
|
|
||||||
# Unicode bullet
|
|
||||||
if stripped.startswith(f"{self._BULLET_DISPLAY} "):
|
|
||||||
return leading_spaces + 2 # bullet + space
|
|
||||||
|
|
||||||
# Markdown bullet list (-, *, +)
|
|
||||||
if re.match(r"^[-*+]\s", stripped):
|
|
||||||
return leading_spaces + 2 # marker + space
|
|
||||||
|
|
||||||
# Numbered list: e.g. "1. "
|
|
||||||
m = re.match(r"^(\d+\.\s)", stripped)
|
|
||||||
if m:
|
|
||||||
return leading_spaces + leading_spaces + (len(m.group(1)) - leading_spaces)
|
|
||||||
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def _detect_list_type(self, line: str) -> tuple[str | None, str]:
|
def _detect_list_type(self, line: str) -> tuple[str | None, str]:
|
||||||
"""
|
"""
|
||||||
Detect if line is a list item. Returns (list_type, prefix).
|
Detect if line is a list item. Returns (list_type, prefix).
|
||||||
|
|
@ -593,102 +559,48 @@ class MarkdownEditor(QTextEdit):
|
||||||
self._update_code_block_row_backgrounds()
|
self._update_code_block_row_backgrounds()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Handle Backspace on empty list items so the marker itself can be deleted
|
# Handle Home and Left arrow keys to prevent going left of list markers
|
||||||
if event.key() == Qt.Key.Key_Backspace:
|
|
||||||
cursor = self.textCursor()
|
|
||||||
# Let Backspace behave normally when deleting a selection.
|
|
||||||
if not cursor.hasSelection():
|
|
||||||
block = cursor.block()
|
|
||||||
prefix_len = self._list_prefix_length_for_block(block)
|
|
||||||
|
|
||||||
if prefix_len > 0:
|
|
||||||
block_start = block.position()
|
|
||||||
line = block.text()
|
|
||||||
pos_in_block = cursor.position() - block_start
|
|
||||||
after_text = line[prefix_len:]
|
|
||||||
|
|
||||||
# If there is no real content after the marker, treat Backspace
|
|
||||||
# as "remove the list marker".
|
|
||||||
if after_text.strip() == "" and pos_in_block >= prefix_len:
|
|
||||||
cursor.beginEditBlock()
|
|
||||||
cursor.setPosition(block_start)
|
|
||||||
cursor.setPosition(
|
|
||||||
block_start + prefix_len, QTextCursor.KeepAnchor
|
|
||||||
)
|
|
||||||
cursor.removeSelectedText()
|
|
||||||
cursor.endEditBlock()
|
|
||||||
self.setTextCursor(cursor)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Handle Home and Left arrow keys to keep the caret to the *right*
|
|
||||||
# of list prefixes (checkboxes / bullets / numbers).
|
|
||||||
if event.key() in (Qt.Key.Key_Home, Qt.Key.Key_Left):
|
if event.key() in (Qt.Key.Key_Home, Qt.Key.Key_Left):
|
||||||
# Let Ctrl+Home / Ctrl+Left keep their usual meaning (start of
|
|
||||||
# document / word-left) – we don't interfere with those.
|
|
||||||
if event.modifiers() & Qt.ControlModifier:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
cursor = self.textCursor()
|
|
||||||
block = cursor.block()
|
|
||||||
prefix_len = self._list_prefix_length_for_block(block)
|
|
||||||
|
|
||||||
if prefix_len > 0:
|
|
||||||
block_start = block.position()
|
|
||||||
pos_in_block = cursor.position() - block_start
|
|
||||||
target = block_start + prefix_len
|
|
||||||
|
|
||||||
if event.key() == Qt.Key.Key_Home:
|
|
||||||
# Home should jump to just after the prefix; with Shift
|
|
||||||
# it should *select* back to that position.
|
|
||||||
if event.modifiers() & Qt.ShiftModifier:
|
|
||||||
cursor.setPosition(target, QTextCursor.KeepAnchor)
|
|
||||||
else:
|
|
||||||
cursor.setPosition(target)
|
|
||||||
self.setTextCursor(cursor)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Left arrow: don't allow the caret to move into the prefix
|
|
||||||
# region; snap it to just after the marker instead.
|
|
||||||
if event.key() == Qt.Key.Key_Left and pos_in_block <= prefix_len:
|
|
||||||
if event.modifiers() & Qt.ShiftModifier:
|
|
||||||
cursor.setPosition(target, QTextCursor.KeepAnchor)
|
|
||||||
else:
|
|
||||||
cursor.setPosition(target)
|
|
||||||
self.setTextCursor(cursor)
|
|
||||||
return
|
|
||||||
|
|
||||||
# After moving vertically, make sure we don't land *inside* a list
|
|
||||||
# prefix. We let QTextEdit perform the move first and then adjust.
|
|
||||||
if event.key() in (Qt.Key.Key_Up, Qt.Key.Key_Down) and not (
|
|
||||||
event.modifiers() & Qt.ControlModifier
|
|
||||||
):
|
|
||||||
super().keyPressEvent(event)
|
|
||||||
|
|
||||||
cursor = self.textCursor()
|
cursor = self.textCursor()
|
||||||
block = cursor.block()
|
block = cursor.block()
|
||||||
|
line = block.text()
|
||||||
|
pos_in_block = cursor.position() - block.position()
|
||||||
|
|
||||||
# Don't interfere with code blocks (they can contain literal
|
# Detect list prefix length
|
||||||
# markdown-looking text).
|
prefix_len = 0
|
||||||
if self._is_inside_code_block(block):
|
stripped = line.lstrip()
|
||||||
return
|
leading_spaces = len(line) - len(stripped)
|
||||||
|
|
||||||
|
# Check for checkbox (Unicode display format)
|
||||||
|
if stripped.startswith(
|
||||||
|
f"{self._CHECK_UNCHECKED_DISPLAY} "
|
||||||
|
) or stripped.startswith(f"{self._CHECK_CHECKED_DISPLAY} "):
|
||||||
|
prefix_len = leading_spaces + 2 # icon + space
|
||||||
|
# Check for Unicode bullet
|
||||||
|
elif stripped.startswith(f"{self._BULLET_DISPLAY} "):
|
||||||
|
prefix_len = leading_spaces + 2 # bullet + space
|
||||||
|
# Check for markdown bullet list (-, *, +)
|
||||||
|
elif re.match(r"^[-*+]\s", stripped):
|
||||||
|
prefix_len = leading_spaces + 2 # marker + space
|
||||||
|
# Check for numbered list
|
||||||
|
elif re.match(r"^\d+\.\s", stripped):
|
||||||
|
match = re.match(r"^(\d+\.\s)", stripped)
|
||||||
|
if match:
|
||||||
|
prefix_len = leading_spaces + len(match.group(1))
|
||||||
|
|
||||||
prefix_len = self._list_prefix_length_for_block(block)
|
|
||||||
if prefix_len > 0:
|
if prefix_len > 0:
|
||||||
block_start = block.position()
|
if event.key() == Qt.Key.Key_Home:
|
||||||
pos_in_block = cursor.position() - block_start
|
# Move to after the list marker
|
||||||
if pos_in_block < prefix_len:
|
cursor.setPosition(block.position() + prefix_len)
|
||||||
target = block_start + prefix_len
|
|
||||||
if event.modifiers() & Qt.ShiftModifier:
|
|
||||||
# Preserve the current anchor while snapping the visual
|
|
||||||
# caret to just after the marker.
|
|
||||||
anchor = cursor.anchor()
|
|
||||||
cursor.setPosition(anchor)
|
|
||||||
cursor.setPosition(target, QTextCursor.KeepAnchor)
|
|
||||||
else:
|
|
||||||
cursor.setPosition(target)
|
|
||||||
self.setTextCursor(cursor)
|
self.setTextCursor(cursor)
|
||||||
|
return
|
||||||
return
|
elif event.key() == Qt.Key.Key_Left and pos_in_block <= prefix_len:
|
||||||
|
# Prevent moving left of the list marker
|
||||||
|
if pos_in_block > prefix_len:
|
||||||
|
# Allow normal left movement if we're past the prefix
|
||||||
|
super().keyPressEvent(event)
|
||||||
|
# Otherwise block the movement
|
||||||
|
return
|
||||||
|
|
||||||
# Handle Enter key for smart list continuation AND code blocks
|
# Handle Enter key for smart list continuation AND code blocks
|
||||||
if event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter:
|
if event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter:
|
||||||
|
|
@ -753,19 +665,6 @@ class MarkdownEditor(QTextEdit):
|
||||||
super().keyPressEvent(event)
|
super().keyPressEvent(event)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Auto-insert an extra blank line after headings (#, ##, ###)
|
|
||||||
# when pressing Enter at the end of the line.
|
|
||||||
if re.match(r"^#{1,3}\s+", stripped) and pos_in_block >= len(line_text):
|
|
||||||
cursor.beginEditBlock()
|
|
||||||
# First blank line: visual separator between heading and body
|
|
||||||
cursor.insertBlock()
|
|
||||||
# Second blank line: where body text will start (caret ends here)
|
|
||||||
cursor.insertBlock()
|
|
||||||
cursor.endEditBlock()
|
|
||||||
|
|
||||||
self.setTextCursor(cursor)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Check for list continuation
|
# Check for list continuation
|
||||||
list_type, prefix = self._detect_list_type(current_line)
|
list_type, prefix = self._detect_list_type(current_line)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,21 +52,21 @@ class TagBrowserDialog(QDialog):
|
||||||
# Tag management buttons
|
# Tag management buttons
|
||||||
btn_row = QHBoxLayout()
|
btn_row = QHBoxLayout()
|
||||||
|
|
||||||
self.add_tag_btn = QPushButton("&" + strings._("add_a_tag"))
|
self.add_tag_btn = QPushButton(strings._("add_a_tag"))
|
||||||
self.add_tag_btn.clicked.connect(self._add_a_tag)
|
self.add_tag_btn.clicked.connect(self._add_a_tag)
|
||||||
btn_row.addWidget(self.add_tag_btn)
|
btn_row.addWidget(self.add_tag_btn)
|
||||||
|
|
||||||
self.edit_name_btn = QPushButton("&" + strings._("edit_tag_name"))
|
self.edit_name_btn = QPushButton(strings._("edit_tag_name"))
|
||||||
self.edit_name_btn.clicked.connect(self._edit_tag_name)
|
self.edit_name_btn.clicked.connect(self._edit_tag_name)
|
||||||
self.edit_name_btn.setEnabled(False)
|
self.edit_name_btn.setEnabled(False)
|
||||||
btn_row.addWidget(self.edit_name_btn)
|
btn_row.addWidget(self.edit_name_btn)
|
||||||
|
|
||||||
self.change_color_btn = QPushButton("&" + strings._("change_color"))
|
self.change_color_btn = QPushButton(strings._("change_color"))
|
||||||
self.change_color_btn.clicked.connect(self._change_tag_color)
|
self.change_color_btn.clicked.connect(self._change_tag_color)
|
||||||
self.change_color_btn.setEnabled(False)
|
self.change_color_btn.setEnabled(False)
|
||||||
btn_row.addWidget(self.change_color_btn)
|
btn_row.addWidget(self.change_color_btn)
|
||||||
|
|
||||||
self.delete_btn = QPushButton("&" + strings._("delete_tag"))
|
self.delete_btn = QPushButton(strings._("delete_tag"))
|
||||||
self.delete_btn.clicked.connect(self._delete_tag)
|
self.delete_btn.clicked.connect(self._delete_tag)
|
||||||
self.delete_btn.setEnabled(False)
|
self.delete_btn.setEnabled(False)
|
||||||
btn_row.addWidget(self.delete_btn)
|
btn_row.addWidget(self.delete_btn)
|
||||||
|
|
|
||||||
|
|
@ -231,14 +231,14 @@ class TimeLogDialog(QDialog):
|
||||||
|
|
||||||
# --- Buttons for entry
|
# --- Buttons for entry
|
||||||
btn_row = QHBoxLayout()
|
btn_row = QHBoxLayout()
|
||||||
self.add_update_btn = QPushButton("&" + strings._("add_time_entry"))
|
self.add_update_btn = QPushButton(strings._("add_time_entry"))
|
||||||
self.add_update_btn.clicked.connect(self._on_add_or_update)
|
self.add_update_btn.clicked.connect(self._on_add_or_update)
|
||||||
|
|
||||||
self.delete_btn = QPushButton("&" + strings._("delete_time_entry"))
|
self.delete_btn = QPushButton(strings._("delete_time_entry"))
|
||||||
self.delete_btn.clicked.connect(self._on_delete_entry)
|
self.delete_btn.clicked.connect(self._on_delete_entry)
|
||||||
self.delete_btn.setEnabled(False)
|
self.delete_btn.setEnabled(False)
|
||||||
|
|
||||||
self.report_btn = QPushButton("&" + strings._("run_report"))
|
self.report_btn = QPushButton(strings._("run_report"))
|
||||||
self.report_btn.clicked.connect(self._on_run_report)
|
self.report_btn.clicked.connect(self._on_run_report)
|
||||||
|
|
||||||
btn_row.addStretch(1)
|
btn_row.addStretch(1)
|
||||||
|
|
@ -274,7 +274,7 @@ class TimeLogDialog(QDialog):
|
||||||
# --- Close button
|
# --- Close button
|
||||||
close_row = QHBoxLayout()
|
close_row = QHBoxLayout()
|
||||||
close_row.addStretch(1)
|
close_row.addStretch(1)
|
||||||
close_btn = QPushButton("&" + strings._("close"))
|
close_btn = QPushButton(strings._("close"))
|
||||||
close_btn.clicked.connect(self.accept)
|
close_btn.clicked.connect(self.accept)
|
||||||
close_row.addWidget(close_btn)
|
close_row.addWidget(close_btn)
|
||||||
root.addLayout(close_row)
|
root.addLayout(close_row)
|
||||||
|
|
@ -333,7 +333,7 @@ class TimeLogDialog(QDialog):
|
||||||
|
|
||||||
self._current_entry_id = None
|
self._current_entry_id = None
|
||||||
self.delete_btn.setEnabled(False)
|
self.delete_btn.setEnabled(False)
|
||||||
self.add_update_btn.setText("&" + strings._("add_time_entry"))
|
self.add_update_btn.setText(strings._("add_time_entry"))
|
||||||
|
|
||||||
# ----- Actions -----------------------------------------------------
|
# ----- Actions -----------------------------------------------------
|
||||||
|
|
||||||
|
|
@ -383,6 +383,7 @@ class TimeLogDialog(QDialog):
|
||||||
self._current_entry_id, proj_id, activity_id, minutes, note
|
self._current_entry_id, proj_id, activity_id, minutes, note
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.note.setText("")
|
||||||
self._reload_entries()
|
self._reload_entries()
|
||||||
|
|
||||||
def _on_row_selected(self) -> None:
|
def _on_row_selected(self) -> None:
|
||||||
|
|
@ -390,7 +391,7 @@ class TimeLogDialog(QDialog):
|
||||||
if not items:
|
if not items:
|
||||||
self._current_entry_id = None
|
self._current_entry_id = None
|
||||||
self.delete_btn.setEnabled(False)
|
self.delete_btn.setEnabled(False)
|
||||||
self.add_update_btn.setText("&" + strings._("add_time_entry"))
|
self.add_update_btn.setText(strings._("add_time_entry"))
|
||||||
return
|
return
|
||||||
|
|
||||||
row = items[0].row()
|
row = items[0].row()
|
||||||
|
|
@ -402,7 +403,7 @@ class TimeLogDialog(QDialog):
|
||||||
|
|
||||||
self._current_entry_id = int(entry_id)
|
self._current_entry_id = int(entry_id)
|
||||||
self.delete_btn.setEnabled(True)
|
self.delete_btn.setEnabled(True)
|
||||||
self.add_update_btn.setText("&" + strings._("update_time_entry"))
|
self.add_update_btn.setText(strings._("update_time_entry"))
|
||||||
|
|
||||||
# push values into the editors
|
# push values into the editors
|
||||||
proj_name = proj_item.text()
|
proj_name = proj_item.text()
|
||||||
|
|
@ -542,15 +543,15 @@ class TimeCodeManagerDialog(QDialog):
|
||||||
proj_layout.addWidget(self.project_list, 1)
|
proj_layout.addWidget(self.project_list, 1)
|
||||||
|
|
||||||
proj_btn_row = QHBoxLayout()
|
proj_btn_row = QHBoxLayout()
|
||||||
self.proj_add_btn = QPushButton("&" + strings._("add_project"))
|
self.proj_add_btn = QPushButton(strings._("add_project"))
|
||||||
self.proj_rename_btn = QPushButton("&" + strings._("rename_project"))
|
self.proj_rename_btn = QPushButton(strings._("rename_project"))
|
||||||
self.proj_delete_btn = QPushButton("&" + strings._("delete_project"))
|
self.proj_delete_btn = QPushButton(strings._("delete_project"))
|
||||||
proj_btn_row.addWidget(self.proj_add_btn)
|
proj_btn_row.addWidget(self.proj_add_btn)
|
||||||
proj_btn_row.addWidget(self.proj_rename_btn)
|
proj_btn_row.addWidget(self.proj_rename_btn)
|
||||||
proj_btn_row.addWidget(self.proj_delete_btn)
|
proj_btn_row.addWidget(self.proj_delete_btn)
|
||||||
proj_layout.addLayout(proj_btn_row)
|
proj_layout.addLayout(proj_btn_row)
|
||||||
|
|
||||||
self.tabs.addTab(proj_tab, "&" + strings._("projects"))
|
self.tabs.addTab(proj_tab, strings._("projects"))
|
||||||
|
|
||||||
# Activities tab
|
# Activities tab
|
||||||
act_tab = QWidget()
|
act_tab = QWidget()
|
||||||
|
|
@ -559,9 +560,9 @@ class TimeCodeManagerDialog(QDialog):
|
||||||
act_layout.addWidget(self.activity_list, 1)
|
act_layout.addWidget(self.activity_list, 1)
|
||||||
|
|
||||||
act_btn_row = QHBoxLayout()
|
act_btn_row = QHBoxLayout()
|
||||||
self.act_add_btn = QPushButton("&" + strings._("add_activity"))
|
self.act_add_btn = QPushButton(strings._("add_activity"))
|
||||||
self.act_rename_btn = QPushButton("&" + strings._("rename_activity"))
|
self.act_rename_btn = QPushButton(strings._("rename_activity"))
|
||||||
self.act_delete_btn = QPushButton("&" + strings._("delete_activity"))
|
self.act_delete_btn = QPushButton(strings._("delete_activity"))
|
||||||
act_btn_row.addWidget(self.act_add_btn)
|
act_btn_row.addWidget(self.act_add_btn)
|
||||||
act_btn_row.addWidget(self.act_rename_btn)
|
act_btn_row.addWidget(self.act_rename_btn)
|
||||||
act_btn_row.addWidget(self.act_delete_btn)
|
act_btn_row.addWidget(self.act_delete_btn)
|
||||||
|
|
@ -572,7 +573,7 @@ class TimeCodeManagerDialog(QDialog):
|
||||||
# Close
|
# Close
|
||||||
close_row = QHBoxLayout()
|
close_row = QHBoxLayout()
|
||||||
close_row.addStretch(1)
|
close_row.addStretch(1)
|
||||||
close_btn = QPushButton("&" + strings._("close"))
|
close_btn = QPushButton(strings._("close"))
|
||||||
close_btn.clicked.connect(self.accept)
|
close_btn.clicked.connect(self.accept)
|
||||||
close_row.addWidget(close_btn)
|
close_row.addWidget(close_btn)
|
||||||
root.addLayout(close_row)
|
root.addLayout(close_row)
|
||||||
|
|
@ -916,7 +917,7 @@ class TimeReportDialog(QDialog):
|
||||||
# Close
|
# Close
|
||||||
close_row = QHBoxLayout()
|
close_row = QHBoxLayout()
|
||||||
close_row.addStretch(1)
|
close_row.addStretch(1)
|
||||||
close_btn = QPushButton("&" + strings._("close"))
|
close_btn = QPushButton(strings._("close"))
|
||||||
close_btn.clicked.connect(self.accept)
|
close_btn.clicked.connect(self.accept)
|
||||||
close_row.addWidget(close_btn)
|
close_row.addWidget(close_btn)
|
||||||
root.addLayout(close_row)
|
root.addLayout(close_row)
|
||||||
|
|
|
||||||
|
|
@ -1659,152 +1659,3 @@ Unicode: 你好 café résumé
|
||||||
doc.setPlainText(text)
|
doc.setPlainText(text)
|
||||||
|
|
||||||
assert highlighter is not None
|
assert highlighter is not None
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"markdown_line",
|
|
||||||
[
|
|
||||||
"- [ ] Task", # checkbox
|
|
||||||
"- Task", # bullet
|
|
||||||
"1. Task", # numbered
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_home_on_list_line_moves_to_text_start(qtbot, editor, markdown_line):
|
|
||||||
"""Home on a list line should jump to just after the list marker."""
|
|
||||||
editor.from_markdown(markdown_line)
|
|
||||||
|
|
||||||
# Put caret at end of the line
|
|
||||||
cursor = editor.textCursor()
|
|
||||||
cursor.movePosition(QTextCursor.End)
|
|
||||||
editor.setTextCursor(cursor)
|
|
||||||
|
|
||||||
# Press Home (no modifiers)
|
|
||||||
qtbot.keyPress(editor, Qt.Key_Home)
|
|
||||||
qtbot.wait(0)
|
|
||||||
|
|
||||||
c = editor.textCursor()
|
|
||||||
block = c.block()
|
|
||||||
line = block.text()
|
|
||||||
pos_in_block = c.position() - block.position()
|
|
||||||
|
|
||||||
# The first character of the user text is the 'T' in "Task"
|
|
||||||
logical_start = line.index("Task")
|
|
||||||
|
|
||||||
assert not c.hasSelection()
|
|
||||||
assert pos_in_block == logical_start
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"markdown_line",
|
|
||||||
[
|
|
||||||
"- [ ] Task", # checkbox
|
|
||||||
"- Task", # bullet
|
|
||||||
"1. Task", # numbered
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_shift_home_on_list_line_selects_text_after_marker(
|
|
||||||
qtbot, editor, markdown_line
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Shift+Home from the end of a list line should select the text after the marker,
|
|
||||||
not the marker itself.
|
|
||||||
"""
|
|
||||||
editor.from_markdown(markdown_line)
|
|
||||||
|
|
||||||
# Put caret at end of the line
|
|
||||||
cursor = editor.textCursor()
|
|
||||||
cursor.movePosition(QTextCursor.End)
|
|
||||||
editor.setTextCursor(cursor)
|
|
||||||
|
|
||||||
# Shift+Home: extend selection back to "logical home"
|
|
||||||
qtbot.keyPress(editor, Qt.Key_Home, Qt.ShiftModifier)
|
|
||||||
qtbot.wait(0)
|
|
||||||
|
|
||||||
c = editor.textCursor()
|
|
||||||
block = c.block()
|
|
||||||
line = block.text()
|
|
||||||
block_start = block.position()
|
|
||||||
|
|
||||||
logical_start = line.index("Task")
|
|
||||||
expected_start = block_start + logical_start
|
|
||||||
expected_end = block_start + len(line)
|
|
||||||
|
|
||||||
assert c.hasSelection()
|
|
||||||
assert c.selectionStart() == expected_start
|
|
||||||
assert c.selectionEnd() == expected_end
|
|
||||||
# Selected text is exactly the user-visible text, not the marker
|
|
||||||
assert c.selectedText() == line[logical_start:]
|
|
||||||
|
|
||||||
|
|
||||||
def test_up_from_below_checkbox_moves_to_text_start(qtbot, editor):
|
|
||||||
"""
|
|
||||||
Up from the line below a checkbox should land to the right of the checkbox,
|
|
||||||
where the text starts, not to the left of the marker.
|
|
||||||
"""
|
|
||||||
editor.from_markdown("- [ ] Task\nSecond line")
|
|
||||||
|
|
||||||
# Put caret somewhere on the second line (end of document is fine)
|
|
||||||
cursor = editor.textCursor()
|
|
||||||
cursor.movePosition(QTextCursor.End)
|
|
||||||
editor.setTextCursor(cursor)
|
|
||||||
|
|
||||||
# Press Up to move to the checkbox line
|
|
||||||
qtbot.keyPress(editor, Qt.Key_Up)
|
|
||||||
qtbot.wait(0)
|
|
||||||
|
|
||||||
c = editor.textCursor()
|
|
||||||
block = c.block()
|
|
||||||
line = block.text()
|
|
||||||
pos_in_block = c.position() - block.position()
|
|
||||||
|
|
||||||
logical_start = line.index("Task")
|
|
||||||
assert pos_in_block >= logical_start
|
|
||||||
|
|
||||||
|
|
||||||
def test_backspace_on_empty_checkbox_removes_marker(qtbot, editor):
|
|
||||||
"""
|
|
||||||
When a checkbox line has no text after the marker, Backspace at/after the
|
|
||||||
text position should delete the marker itself, leaving a plain empty line.
|
|
||||||
"""
|
|
||||||
editor.from_markdown("- [ ] ")
|
|
||||||
|
|
||||||
# Put caret at end of the checkbox line (after the marker)
|
|
||||||
cursor = editor.textCursor()
|
|
||||||
cursor.movePosition(QTextCursor.End)
|
|
||||||
editor.setTextCursor(cursor)
|
|
||||||
|
|
||||||
qtbot.keyPress(editor, Qt.Key_Backspace)
|
|
||||||
qtbot.wait(0)
|
|
||||||
|
|
||||||
first_block = editor.document().firstBlock()
|
|
||||||
# Marker should be gone
|
|
||||||
assert first_block.text() == ""
|
|
||||||
assert editor._CHECK_UNCHECKED_DISPLAY not in editor.toPlainText()
|
|
||||||
|
|
||||||
|
|
||||||
def test_insert_alarm_marker_on_checkbox_line_does_not_merge_lines(editor, qtbot):
|
|
||||||
# Two checkbox lines
|
|
||||||
editor.from_markdown("- [ ] Test\n- [ ] Foobar")
|
|
||||||
|
|
||||||
# Move caret to second line ("Foobar")
|
|
||||||
cursor = editor.textCursor()
|
|
||||||
cursor.movePosition(QTextCursor.Start)
|
|
||||||
cursor.movePosition(QTextCursor.Down)
|
|
||||||
editor.setTextCursor(cursor)
|
|
||||||
|
|
||||||
# Add an alarm to the second line
|
|
||||||
editor.insert_alarm_marker("16:54")
|
|
||||||
qtbot.wait(0)
|
|
||||||
|
|
||||||
lines = lines_keep(editor)
|
|
||||||
|
|
||||||
# Still two separate lines
|
|
||||||
assert len(lines) == 2
|
|
||||||
|
|
||||||
# First line unchanged (no alarm)
|
|
||||||
assert "Test" in lines[0]
|
|
||||||
assert "⏰" not in lines[0]
|
|
||||||
|
|
||||||
# Second line has the alarm marker
|
|
||||||
assert "Foobar" in lines[1]
|
|
||||||
assert "⏰ 16:54" in lines[1]
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue