PayPerTranscript 0.2.7__tar.gz → 0.2.8__tar.gz
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.
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/PKG-INFO +1 -1
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/PayPerTranscript.egg-info/PKG-INFO +1 -1
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/PayPerTranscript.egg-info/SOURCES.txt +2 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/__init__.py +1 -1
- paypertranscript-0.2.8/paypertranscript/assets/icons/tray_green.png +0 -0
- paypertranscript-0.2.8/paypertranscript/assets/icons/tray_orange.png +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/core/config.py +12 -1
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/ui/main_window.py +64 -16
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/ui/pages/window_mapping_page.py +65 -39
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/ui/pages/word_list_page.py +29 -20
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/ui/tray.py +23 -68
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/pyproject.toml +1 -1
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/LICENSE +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/PayPerTranscript.egg-info/dependency_links.txt +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/PayPerTranscript.egg-info/entry_points.txt +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/PayPerTranscript.egg-info/requires.txt +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/PayPerTranscript.egg-info/top_level.txt +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/README.md +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/__main__.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/assets/icons/app.ico +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/assets/icons/app.png +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/assets/icons/app_big.png +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/assets/icons/arrow_down.svg +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/assets/icons/tray.png +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/assets/sounds/start.wav +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/assets/sounds/stop.wav +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/assets/styles/dark.qss +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/core/__init__.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/core/audio_manager.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/core/cost_tracker.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/core/hotkey.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/core/logging.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/core/paths.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/core/recorder.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/core/session_logger.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/core/text_inserter.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/core/updater.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/core/window_detector.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/pipeline/__init__.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/pipeline/transcription.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/providers/__init__.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/providers/base.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/providers/groq_provider.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/ui/__init__.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/ui/animated.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/ui/app.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/ui/constants.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/ui/overlay.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/ui/pages/__init__.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/ui/pages/home_page.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/ui/pages/settings_page.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/ui/pages/statistics_page.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/ui/setup_wizard.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/ui/sidebar.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/ui/widgets.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.8}/setup.cfg +0 -0
|
@@ -14,6 +14,8 @@ paypertranscript/assets/icons/app.png
|
|
|
14
14
|
paypertranscript/assets/icons/app_big.png
|
|
15
15
|
paypertranscript/assets/icons/arrow_down.svg
|
|
16
16
|
paypertranscript/assets/icons/tray.png
|
|
17
|
+
paypertranscript/assets/icons/tray_green.png
|
|
18
|
+
paypertranscript/assets/icons/tray_orange.png
|
|
17
19
|
paypertranscript/assets/sounds/start.wav
|
|
18
20
|
paypertranscript/assets/sounds/stop.wav
|
|
19
21
|
paypertranscript/assets/styles/dark.qss
|
|
Binary file
|
|
Binary file
|
|
@@ -97,11 +97,22 @@ _SCHEMA: dict[str, type | tuple[type, ...]] = {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
|
|
100
|
+
# Keys whose dict values represent user data collections (not config structure).
|
|
101
|
+
# These are replaced entirely by the user's saved value, not recursively merged,
|
|
102
|
+
# so that deletions of default entries persist across restarts.
|
|
103
|
+
_REPLACE_KEYS = frozenset({"window_mappings", "categories"})
|
|
104
|
+
|
|
105
|
+
|
|
100
106
|
def _deep_merge(base: dict, override: dict) -> dict:
|
|
101
107
|
"""Merge override in base (rekursiv). Gibt neues Dict zurück."""
|
|
102
108
|
result = copy.deepcopy(base)
|
|
103
109
|
for key, value in override.items():
|
|
104
|
-
if
|
|
110
|
+
if (
|
|
111
|
+
key in result
|
|
112
|
+
and isinstance(result[key], dict)
|
|
113
|
+
and isinstance(value, dict)
|
|
114
|
+
and key not in _REPLACE_KEYS
|
|
115
|
+
):
|
|
105
116
|
result[key] = _deep_merge(result[key], value)
|
|
106
117
|
else:
|
|
107
118
|
result[key] = copy.deepcopy(value)
|
|
@@ -16,10 +16,6 @@ from paypertranscript.core.logging import get_logger
|
|
|
16
16
|
from paypertranscript.core.paths import get_icons_dir
|
|
17
17
|
from paypertranscript.core.session_logger import SessionLogger
|
|
18
18
|
from paypertranscript.ui.pages.home_page import HomePage
|
|
19
|
-
from paypertranscript.ui.pages.settings_page import SettingsPage
|
|
20
|
-
from paypertranscript.ui.pages.statistics_page import StatisticsPage
|
|
21
|
-
from paypertranscript.ui.pages.window_mapping_page import WindowMappingPage
|
|
22
|
-
from paypertranscript.ui.pages.word_list_page import WordListPage
|
|
23
19
|
from paypertranscript.ui.sidebar import Sidebar
|
|
24
20
|
|
|
25
21
|
log = get_logger("ui.main_window")
|
|
@@ -86,29 +82,81 @@ class MainWindow(QDialog):
|
|
|
86
82
|
self._stack = QStackedWidget()
|
|
87
83
|
self._stack.setStyleSheet("QStackedWidget { background-color: #121218; }")
|
|
88
84
|
|
|
89
|
-
#
|
|
90
|
-
self.
|
|
91
|
-
self.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
85
|
+
# Lazy Page Loading: Nur HomePage sofort erstellen, Rest on-demand.
|
|
86
|
+
self._pages: dict[int, QWidget] = {}
|
|
87
|
+
self._page_factories: dict[int, Callable[[], QWidget]] = {
|
|
88
|
+
1: lambda: self._create_statistics_page(session_logger),
|
|
89
|
+
2: lambda: self._create_settings_page(config, hotkey_listener),
|
|
90
|
+
3: lambda: self._create_word_list_page(config),
|
|
91
|
+
4: lambda: self._create_window_mapping_page(config),
|
|
92
|
+
}
|
|
95
93
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
self.
|
|
99
|
-
self._stack.addWidget(
|
|
100
|
-
|
|
94
|
+
# HomePage (Index 0) sofort — wird bei jedem Oeffnen angezeigt
|
|
95
|
+
home = HomePage(config, session_logger, get_last_transcription)
|
|
96
|
+
self._pages[0] = home
|
|
97
|
+
self._stack.addWidget(home) # 0
|
|
98
|
+
|
|
99
|
+
# Placeholders fuer restliche Seiten
|
|
100
|
+
for _ in range(1, 5):
|
|
101
|
+
self._stack.addWidget(QWidget())
|
|
101
102
|
|
|
102
103
|
layout.addWidget(self._stack, 1)
|
|
103
104
|
|
|
105
|
+
# -- Lazy Page Factories --
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
def _create_statistics_page(session_logger: SessionLogger | None) -> QWidget:
|
|
109
|
+
from paypertranscript.ui.pages.statistics_page import StatisticsPage
|
|
110
|
+
|
|
111
|
+
return StatisticsPage(session_logger) if session_logger else QWidget()
|
|
112
|
+
|
|
113
|
+
@staticmethod
|
|
114
|
+
def _create_settings_page(
|
|
115
|
+
config: ConfigManager, hotkey_listener: HotkeyListener | None
|
|
116
|
+
) -> QWidget:
|
|
117
|
+
from paypertranscript.ui.pages.settings_page import SettingsPage
|
|
118
|
+
|
|
119
|
+
return SettingsPage(config, hotkey_listener)
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def _create_word_list_page(config: ConfigManager) -> QWidget:
|
|
123
|
+
from paypertranscript.ui.pages.word_list_page import WordListPage
|
|
124
|
+
|
|
125
|
+
return WordListPage(config)
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def _create_window_mapping_page(config: ConfigManager) -> QWidget:
|
|
129
|
+
from paypertranscript.ui.pages.window_mapping_page import WindowMappingPage
|
|
130
|
+
|
|
131
|
+
return WindowMappingPage(config)
|
|
132
|
+
|
|
133
|
+
def _ensure_page(self, index: int) -> None:
|
|
134
|
+
"""Erstellt die Seite beim ersten Zugriff (lazy loading)."""
|
|
135
|
+
if index in self._pages:
|
|
136
|
+
return
|
|
137
|
+
factory = self._page_factories.pop(index, None)
|
|
138
|
+
if factory is None:
|
|
139
|
+
return
|
|
140
|
+
page = factory()
|
|
141
|
+
self._pages[index] = page
|
|
142
|
+
old = self._stack.widget(index)
|
|
143
|
+
self._stack.removeWidget(old)
|
|
144
|
+
self._stack.insertWidget(index, page)
|
|
145
|
+
old.deleteLater()
|
|
146
|
+
log.debug("Seite %d lazy erstellt: %s", index, type(page).__name__)
|
|
147
|
+
|
|
148
|
+
# -- Navigation --
|
|
149
|
+
|
|
104
150
|
def _on_page_changed(self, index: int) -> None:
|
|
105
|
-
"""Wechselt zur angeforderten Seite."""
|
|
151
|
+
"""Wechselt zur angeforderten Seite (lazy creation bei Erstaufruf)."""
|
|
152
|
+
self._ensure_page(index)
|
|
106
153
|
self._stack.setCurrentIndex(index)
|
|
107
154
|
self._sidebar.set_active(index)
|
|
108
155
|
|
|
109
156
|
def navigate_to(self, page_index: int) -> None:
|
|
110
157
|
"""Navigiert zu einer bestimmten Seite und zeigt das Fenster."""
|
|
111
158
|
if 0 <= page_index < self._stack.count():
|
|
159
|
+
self._ensure_page(page_index)
|
|
112
160
|
self._stack.setCurrentIndex(page_index)
|
|
113
161
|
self._sidebar.set_active(page_index)
|
|
114
162
|
self.show()
|
{paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/ui/pages/window_mapping_page.py
RENAMED
|
@@ -166,30 +166,18 @@ class _MappingItemDelegate(QStyledItemDelegate):
|
|
|
166
166
|
option: QStyleOptionViewItem,
|
|
167
167
|
index: QModelIndex,
|
|
168
168
|
) -> bool:
|
|
169
|
-
|
|
169
|
+
# Click handling is done via viewport event filter in WindowMappingPage
|
|
170
|
+
# (editorEvent does not receive MouseButtonRelease with default edit triggers)
|
|
171
|
+
if event.type() == QEvent.Type.MouseMove:
|
|
170
172
|
mouse: QMouseEvent = event # type: ignore[assignment]
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if over_icon:
|
|
180
|
-
view.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
|
|
181
|
-
else:
|
|
182
|
-
view.unsetCursor()
|
|
183
|
-
return False
|
|
184
|
-
|
|
185
|
-
# MouseButtonRelease
|
|
186
|
-
if x >= delete_left:
|
|
187
|
-
self._on_delete(index.row())
|
|
188
|
-
return True
|
|
189
|
-
if x >= edit_left:
|
|
190
|
-
self._on_edit(index.row())
|
|
191
|
-
return True
|
|
192
|
-
|
|
173
|
+
over_icon = mouse.position().x() >= option.rect.right() - self._ICONS_TOTAL
|
|
174
|
+
view = option.widget
|
|
175
|
+
if view is not None:
|
|
176
|
+
if over_icon:
|
|
177
|
+
view.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
|
|
178
|
+
else:
|
|
179
|
+
view.unsetCursor()
|
|
180
|
+
return False
|
|
193
181
|
return super().editorEvent(event, model, option, index)
|
|
194
182
|
|
|
195
183
|
def sizeHint(self, option: QStyleOptionViewItem, index: QModelIndex) -> QSize:
|
|
@@ -263,24 +251,17 @@ class _CategoryItemDelegate(QStyledItemDelegate):
|
|
|
263
251
|
option: QStyleOptionViewItem,
|
|
264
252
|
index: QModelIndex,
|
|
265
253
|
) -> bool:
|
|
266
|
-
|
|
254
|
+
# Click handling is done via viewport event filter in WindowMappingPage
|
|
255
|
+
if event.type() == QEvent.Type.MouseMove:
|
|
267
256
|
mouse: QMouseEvent = event # type: ignore[assignment]
|
|
268
257
|
over_icon = mouse.position().x() >= option.rect.right() - self._ICON_SIZE
|
|
269
|
-
|
|
270
|
-
if
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
view.unsetCursor()
|
|
277
|
-
return False
|
|
278
|
-
|
|
279
|
-
# MouseButtonRelease
|
|
280
|
-
if over_icon:
|
|
281
|
-
self._on_delete(index.row())
|
|
282
|
-
return True
|
|
283
|
-
|
|
258
|
+
view = option.widget
|
|
259
|
+
if view is not None:
|
|
260
|
+
if over_icon:
|
|
261
|
+
view.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
|
|
262
|
+
else:
|
|
263
|
+
view.unsetCursor()
|
|
264
|
+
return False
|
|
284
265
|
return super().editorEvent(event, model, option, index)
|
|
285
266
|
|
|
286
267
|
def sizeHint(self, option: QStyleOptionViewItem, index: QModelIndex) -> QSize:
|
|
@@ -321,6 +302,11 @@ class WindowMappingPage(QWidget):
|
|
|
321
302
|
self._tabs.addTab(self._create_categories_tab(), "Kategorien")
|
|
322
303
|
layout.addWidget(self._tabs, 1)
|
|
323
304
|
|
|
305
|
+
# Viewport event filters for reliable icon click handling
|
|
306
|
+
# (editorEvent does not receive MouseButtonRelease with default edit triggers)
|
|
307
|
+
self._mapping_list.viewport().installEventFilter(self)
|
|
308
|
+
self._cat_list.viewport().installEventFilter(self)
|
|
309
|
+
|
|
324
310
|
# -- Tab: Zuordnungen --
|
|
325
311
|
|
|
326
312
|
def _create_mappings_tab(self) -> QWidget:
|
|
@@ -438,6 +424,46 @@ class WindowMappingPage(QWidget):
|
|
|
438
424
|
|
|
439
425
|
return tab
|
|
440
426
|
|
|
427
|
+
# -- Viewport event filter: icon clicks --
|
|
428
|
+
|
|
429
|
+
def eventFilter(self, watched: object, event: QEvent) -> bool:
|
|
430
|
+
"""Intercept mouse clicks on delegate icons (edit/delete)."""
|
|
431
|
+
if event.type() == QEvent.Type.MouseButtonRelease:
|
|
432
|
+
mouse: QMouseEvent = event # type: ignore[assignment]
|
|
433
|
+
if watched is self._mapping_list.viewport():
|
|
434
|
+
return self._handle_mapping_icon_click(mouse)
|
|
435
|
+
if watched is self._cat_list.viewport():
|
|
436
|
+
return self._handle_category_icon_click(mouse)
|
|
437
|
+
return super().eventFilter(watched, event)
|
|
438
|
+
|
|
439
|
+
def _handle_mapping_icon_click(self, event: QMouseEvent) -> bool:
|
|
440
|
+
pos = event.position().toPoint()
|
|
441
|
+
index = self._mapping_list.indexAt(pos)
|
|
442
|
+
if not index.isValid():
|
|
443
|
+
return False
|
|
444
|
+
rect = self._mapping_list.visualRect(index)
|
|
445
|
+
x = event.position().x()
|
|
446
|
+
delete_left = rect.right() - _MappingItemDelegate._ICON_SIZE
|
|
447
|
+
edit_left = rect.right() - _MappingItemDelegate._ICONS_TOTAL
|
|
448
|
+
if x >= delete_left:
|
|
449
|
+
self._remove_mapping_at(index.row())
|
|
450
|
+
return True
|
|
451
|
+
if x >= edit_left:
|
|
452
|
+
self._edit_mapping_at(index.row())
|
|
453
|
+
return True
|
|
454
|
+
return False
|
|
455
|
+
|
|
456
|
+
def _handle_category_icon_click(self, event: QMouseEvent) -> bool:
|
|
457
|
+
pos = event.position().toPoint()
|
|
458
|
+
index = self._cat_list.indexAt(pos)
|
|
459
|
+
if not index.isValid():
|
|
460
|
+
return False
|
|
461
|
+
rect = self._cat_list.visualRect(index)
|
|
462
|
+
if event.position().x() >= rect.right() - _CategoryItemDelegate._ICON_SIZE:
|
|
463
|
+
self._delete_category_at(index.row())
|
|
464
|
+
return True
|
|
465
|
+
return False
|
|
466
|
+
|
|
441
467
|
# -- Daten laden --
|
|
442
468
|
|
|
443
469
|
def _reload_all(self) -> None:
|
{paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/ui/pages/word_list_page.py
RENAMED
|
@@ -110,27 +110,17 @@ class _WordItemDelegate(QStyledItemDelegate):
|
|
|
110
110
|
option: QStyleOptionViewItem,
|
|
111
111
|
index: QModelIndex,
|
|
112
112
|
) -> bool:
|
|
113
|
-
"""
|
|
114
|
-
if event.type()
|
|
113
|
+
"""Cursor change on hover; click handling via viewport event filter."""
|
|
114
|
+
if event.type() == QEvent.Type.MouseMove:
|
|
115
115
|
mouse: QMouseEvent = event # type: ignore[assignment]
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
else:
|
|
125
|
-
view.unsetCursor()
|
|
126
|
-
return False
|
|
127
|
-
|
|
128
|
-
# MouseButtonRelease
|
|
129
|
-
if over_icon:
|
|
130
|
-
view = option.widget
|
|
131
|
-
if view is not None:
|
|
132
|
-
view.edit(index)
|
|
133
|
-
return True
|
|
116
|
+
over_icon = mouse.position().x() >= option.rect.right() - self._EDIT_WIDTH
|
|
117
|
+
view = option.widget
|
|
118
|
+
if view is not None:
|
|
119
|
+
if over_icon:
|
|
120
|
+
view.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
|
|
121
|
+
else:
|
|
122
|
+
view.unsetCursor()
|
|
123
|
+
return False
|
|
134
124
|
return super().editorEvent(event, model, option, index)
|
|
135
125
|
|
|
136
126
|
def sizeHint(self, option: QStyleOptionViewItem, index: QModelIndex) -> QSize:
|
|
@@ -200,6 +190,9 @@ class WordListPage(QWidget):
|
|
|
200
190
|
self._list.itemChanged.connect(self._on_word_edited)
|
|
201
191
|
layout.addWidget(self._list, 1)
|
|
202
192
|
|
|
193
|
+
# Viewport event filter for reliable edit icon click handling
|
|
194
|
+
self._list.viewport().installEventFilter(self)
|
|
195
|
+
|
|
203
196
|
# Entfernen + Token-Counter
|
|
204
197
|
action_row = QHBoxLayout()
|
|
205
198
|
btn_remove = QPushButton("Entfernen")
|
|
@@ -213,6 +206,22 @@ class WordListPage(QWidget):
|
|
|
213
206
|
action_row.addWidget(self._counter_label)
|
|
214
207
|
layout.addLayout(action_row)
|
|
215
208
|
|
|
209
|
+
def eventFilter(self, watched: object, event: QEvent) -> bool:
|
|
210
|
+
"""Intercept mouse clicks on the edit (pencil) icon."""
|
|
211
|
+
if (
|
|
212
|
+
event.type() == QEvent.Type.MouseButtonRelease
|
|
213
|
+
and watched is self._list.viewport()
|
|
214
|
+
):
|
|
215
|
+
mouse: QMouseEvent = event # type: ignore[assignment]
|
|
216
|
+
pos = mouse.position().toPoint()
|
|
217
|
+
index = self._list.indexAt(pos)
|
|
218
|
+
if index.isValid():
|
|
219
|
+
rect = self._list.visualRect(index)
|
|
220
|
+
if mouse.position().x() >= rect.right() - _WordItemDelegate._EDIT_WIDTH:
|
|
221
|
+
self._list.edit(index)
|
|
222
|
+
return True
|
|
223
|
+
return super().eventFilter(watched, event)
|
|
224
|
+
|
|
216
225
|
def _load_words(self) -> None:
|
|
217
226
|
self._updating = True
|
|
218
227
|
words = self._config.get("words.misspelled_words", [])
|
|
@@ -5,8 +5,8 @@ QSystemTrayIcon mit Kontextmenue und Icon-Zustandswechsel.
|
|
|
5
5
|
|
|
6
6
|
from collections.abc import Callable
|
|
7
7
|
|
|
8
|
-
from PySide6.QtCore import
|
|
9
|
-
from PySide6.QtGui import
|
|
8
|
+
from PySide6.QtCore import Qt, Slot
|
|
9
|
+
from PySide6.QtGui import QIcon, QPixmap
|
|
10
10
|
from PySide6.QtWidgets import (
|
|
11
11
|
QHBoxLayout,
|
|
12
12
|
QLabel,
|
|
@@ -27,84 +27,39 @@ from paypertranscript.core.session_logger import SessionLogger
|
|
|
27
27
|
|
|
28
28
|
log = get_logger("ui.tray")
|
|
29
29
|
|
|
30
|
-
#
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
# Icon-Dateinamen fuer die drei Zustaende
|
|
31
|
+
_ICON_FILES: dict[str, str] = {
|
|
32
|
+
"idle": "tray.png",
|
|
33
|
+
"recording": "tray_orange.png",
|
|
34
|
+
"processing": "tray_green.png",
|
|
35
|
+
}
|
|
33
36
|
|
|
34
|
-
# Dot-Groesse relativ zum Icon
|
|
35
|
-
_DOT_RADIUS_RATIO = 0.30 # 30% der Icon-Groesse (~60% Durchmesser)
|
|
36
|
-
_DOT_BORDER_RATIO = 0.04 # Dunkler Rand um den Dot
|
|
37
37
|
|
|
38
|
+
def _load_icon(filename: str, size: int = 64) -> QIcon:
|
|
39
|
+
"""Laedt ein Tray-Icon aus den Assets.
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
Sucht nach tray.png in den Assets. Fallback: weisser Kreis.
|
|
41
|
+
Args:
|
|
42
|
+
filename: Dateiname im icons-Ordner.
|
|
43
|
+
size: Zielgroesse in Pixeln.
|
|
43
44
|
"""
|
|
44
|
-
|
|
45
|
-
if
|
|
46
|
-
|
|
47
|
-
if not
|
|
48
|
-
|
|
45
|
+
path = get_icons_dir() / filename
|
|
46
|
+
if path.exists():
|
|
47
|
+
pixmap = QPixmap(str(path))
|
|
48
|
+
if not pixmap.isNull():
|
|
49
|
+
scaled = pixmap.scaled(
|
|
49
50
|
size, size,
|
|
50
51
|
Qt.AspectRatioMode.KeepAspectRatio,
|
|
51
52
|
Qt.TransformationMode.SmoothTransformation,
|
|
52
53
|
)
|
|
54
|
+
return QIcon(scaled)
|
|
53
55
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
pixmap.fill(QColor(0, 0, 0, 0))
|
|
57
|
-
painter = QPainter(pixmap)
|
|
58
|
-
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
59
|
-
painter.setBrush(QColor("#ffffff"))
|
|
60
|
-
painter.setPen(QColor(0, 0, 0, 0))
|
|
61
|
-
painter.drawEllipse(4, 4, size - 8, size - 8)
|
|
62
|
-
painter.end()
|
|
63
|
-
return pixmap
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def _create_icon_with_dot(base: QPixmap, dot_color: str | None = None) -> QIcon:
|
|
67
|
-
"""Erstellt ein Tray-Icon aus dem Basis-Logo, optional mit Status-Dot.
|
|
68
|
-
|
|
69
|
-
Args:
|
|
70
|
-
base: Basis-Pixmap (Logo).
|
|
71
|
-
dot_color: Hex-Farbe fuer den Status-Dot. None = kein Dot (idle).
|
|
72
|
-
"""
|
|
73
|
-
pixmap = base.copy()
|
|
74
|
-
|
|
75
|
-
if dot_color is not None:
|
|
76
|
-
size = pixmap.width()
|
|
77
|
-
dot_r = size * _DOT_RADIUS_RATIO
|
|
78
|
-
border_w = max(1.0, size * _DOT_BORDER_RATIO)
|
|
79
|
-
|
|
80
|
-
# Position: unten rechts mit kleinem Abstand zum Rand
|
|
81
|
-
cx = size - dot_r - border_w - 1
|
|
82
|
-
cy = size - dot_r - border_w - 1
|
|
83
|
-
|
|
84
|
-
painter = QPainter(pixmap)
|
|
85
|
-
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
86
|
-
|
|
87
|
-
# Dunkler Rand (hebt den Dot vom Logo ab)
|
|
88
|
-
painter.setPen(QPen(QColor(18, 18, 24, 200), border_w))
|
|
89
|
-
painter.setBrush(QColor(dot_color))
|
|
90
|
-
painter.drawEllipse(QRectF(cx - dot_r, cy - dot_r, dot_r * 2, dot_r * 2))
|
|
91
|
-
|
|
92
|
-
painter.end()
|
|
93
|
-
|
|
94
|
-
return QIcon(pixmap)
|
|
56
|
+
log.warning("Tray-Icon nicht gefunden: %s", path)
|
|
57
|
+
return QIcon()
|
|
95
58
|
|
|
96
59
|
|
|
97
60
|
def create_tray_icons() -> dict[str, QIcon]:
|
|
98
|
-
"""
|
|
99
|
-
|
|
100
|
-
Idle: Logo pur. Recording: Logo + Emerald-Dot. Processing: Logo + Grau-Dot.
|
|
101
|
-
"""
|
|
102
|
-
base = _load_base_pixmap(64)
|
|
103
|
-
return {
|
|
104
|
-
"idle": _create_icon_with_dot(base),
|
|
105
|
-
"recording": _create_icon_with_dot(base, _DOT_RECORDING),
|
|
106
|
-
"processing": _create_icon_with_dot(base, _DOT_PROCESSING),
|
|
107
|
-
}
|
|
61
|
+
"""Laedt alle Tray-Icons (idle, recording, processing) aus PNG-Dateien."""
|
|
62
|
+
return {state: _load_icon(fname) for state, fname in _ICON_FILES.items()}
|
|
108
63
|
|
|
109
64
|
|
|
110
65
|
# ---------------------------------------------------------------------------
|
|
File without changes
|
{paypertranscript-0.2.7 → paypertranscript-0.2.8}/PayPerTranscript.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{paypertranscript-0.2.7 → paypertranscript-0.2.8}/PayPerTranscript.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/assets/icons/arrow_down.svg
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/pipeline/transcription.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/providers/groq_provider.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/ui/pages/settings_page.py
RENAMED
|
File without changes
|
{paypertranscript-0.2.7 → paypertranscript-0.2.8}/paypertranscript/ui/pages/statistics_page.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|