• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dynobo / normcap / 14280746188

05 Apr 2025 09:50AM UTC coverage: 84.841% (-2.2%) from 87.002%
14280746188

Pull #690

github

web-flow
Merge d28da9558 into c7b17ed4a
Pull Request #690: feature/add-qr-support

318 of 410 new or added lines in 35 files covered. (77.56%)

5 existing lines in 2 files now uncovered.

2569 of 3028 relevant lines covered (84.84%)

3.24 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.84
/normcap/gui/language_manager.py
1
"""Window for managing downloaded language files."""
2

3
import logging
4✔
4
from pathlib import Path
4✔
5
from typing import Optional, Union
4✔
6

7
from PySide6 import QtCore, QtWidgets
4✔
8

9
from normcap.gui import constants
4✔
10
from normcap.gui.downloader import Downloader
4✔
11
from normcap.gui.loading_indicator import LoadingIndicator
4✔
12
from normcap.gui.localization import _
4✔
13

14
logger = logging.getLogger(__name__)
4✔
15

16

17
class Communicate(QtCore.QObject):
4✔
18
    """LanguagesWindow' communication bus."""
19

20
    on_open_url = QtCore.Signal(str)
4✔
21
    on_languages_changed = QtCore.Signal(list)
4✔
22

23

24
class LanguageManager(QtWidgets.QDialog):
4✔
25
    def __init__(
4✔
26
        self, tessdata_path: Path, parent: Optional[QtWidgets.QWidget] = None
27
    ) -> None:
28
        super().__init__(parent=parent)
4✔
29

30
        self.setModal(True)
4✔
31
        # L10N: Title of Language Manager
32
        self.setWindowTitle(_("Manage Languages"))
4✔
33
        self.setMinimumSize(800, 600)
4✔
34

35
        self.tessdata_path = tessdata_path
4✔
36

37
        self.com = Communicate(parent=self)
4✔
38
        self.downloader = Downloader(parent=self)
4✔
39
        self.downloader.com.on_download_failed.connect(self._on_download_error)
4✔
40
        self.downloader.com.on_download_finished.connect(self._on_download_finished)
4✔
41

42
        self.installed_layout = LanguageLayout(
4✔
43
            label_icon="SP_DialogApplyButton",
44
            # L10N: Language Manager section
45
            label_text=_("Installed:"),
46
            button_icon="SP_DialogDiscardButton",
47
            # L10N: Language Manager button
48
            button_text=_("Delete"),
49
        )
50
        self.installed_layout.button.pressed.connect(self._on_delete_btn_clicked)
4✔
51

52
        self.available_layout = LanguageLayout(
4✔
53
            label_icon="SP_ArrowDown",
54
            # L10N: Language Manager section
55
            label_text=_("Available:"),
56
            button_icon="SP_ArrowDown",
57
            # L10N: Language Manager button
58
            button_text=_("Download"),
59
        )
60
        self.available_layout.button.pressed.connect(self._on_download_btn_clicked)
4✔
61

62
        h_layout = QtWidgets.QHBoxLayout()
4✔
63
        h_layout.addLayout(self.installed_layout)
4✔
64
        h_layout.addLayout(self.available_layout)
4✔
65

66
        # L10N: Language Manager button
67
        close_button = QtWidgets.QPushButton(_("Close"))
4✔
68
        close_button.pressed.connect(self.close)
4✔
69

70
        self.tessdata_label = QtWidgets.QLabel(
4✔
71
            f"<a href='file:///{self.tessdata_path.resolve()}'>"
72
            # L10N: Language Manager link to directory on file system
73
            + _("Close and view tessdata folder in file manager...")
74
            + "</a>"
75
        )
76
        self.tessdata_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight)
4✔
77
        self.tessdata_label.linkActivated.connect(self.com.on_open_url)
4✔
78
        self.com.on_open_url.connect(self.close)
4✔
79

80
        layout = QtWidgets.QVBoxLayout()
4✔
81
        layout.addLayout(h_layout)
4✔
82
        layout.addWidget(close_button)
4✔
83
        layout.addWidget(self.tessdata_label)
4✔
84

85
        self.setLayout(layout)
4✔
86

87
        self.loading_indicator = LoadingIndicator(parent=self, size=256)
4✔
88

89
        self._update_models()
4✔
90
        close_button.setFocus()
4✔
91

92
    @QtCore.Slot(str, str)  # type: ignore  # pyside typhint bug?
4✔
93
    def _on_download_error(self, reason: str, url: str) -> None:
4✔
94
        self._set_in_progress(False)
4✔
95
        QtWidgets.QMessageBox.critical(
4✔
96
            self,
97
            # L10N: Language Manager error message box title
98
            _("Error"),
99
            # L10N: Language Manager error message box text
100
            "<b>" + _("Language download failed!") + f"</b><br><br>{reason}",
101
        )
102

103
    @QtCore.Slot(bytes, str)  # type: ignore  # pyside typhint bug?
4✔
104
    def _on_download_finished(self, data: bytes, url: str) -> None:
4✔
105
        """Save language to tessdata folder."""
106
        file_name = url.split("/")[-1]
4✔
107
        with Path(self.tessdata_path / file_name).open(mode="wb") as fh:
4✔
108
            fh.write(data)
4✔
109
        self._update_models()
4✔
110
        self._set_in_progress(False)
4✔
111

112
    @QtCore.Slot()
4✔
113
    def _on_download_btn_clicked(self) -> None:
4✔
114
        if indexes := self.available_layout.view.selectedIndexes():
4✔
115
            self._set_in_progress(True)
4✔
116
            index = indexes[0]
4✔
117
            language = self.available_layout.model.languages[index.row()][0]
4✔
118
            self.downloader.get(constants.TESSDATA_BASE_URL + language + ".traineddata")
4✔
119

120
    @QtCore.Slot()
4✔
121
    def _on_delete_btn_clicked(self) -> None:
4✔
122
        indexes = self.installed_layout.view.selectedIndexes()
4✔
123
        if not indexes:
4✔
124
            return
4✔
125

126
        if len(self.installed_layout.model.languages) <= 1:
4✔
127
            QtWidgets.QMessageBox.information(
4✔
128
                self,
129
                # L10N: Language Manager information message box title
130
                _("Information"),
131
                # L10N: Language Manager information message box text
132
                _(
133
                    "It is not possible to delete all languages. "
134
                    "NormCap needs at least one to function correctly."
135
                ),
136
                QtWidgets.QMessageBox.StandardButton.Close,
137
            )
138
            return
4✔
139

140
        index = indexes[0]
4✔
141
        language = self.installed_layout.model.languages[index.row()][0]
4✔
142
        Path(self.tessdata_path / f"{language}.traineddata").unlink()
4✔
143
        self._update_models()
4✔
144

145
    def _update_models(self) -> None:
4✔
146
        installed = self._get_installed_languages()
4✔
147
        self.installed_layout.model.languages = [
4✔
148
            lang for lang in constants.LANGUAGES if lang[0] in installed
149
        ]
150
        self.available_layout.model.languages = [
4✔
151
            lang for lang in constants.LANGUAGES if lang[0] not in installed
152
        ]
153
        self.installed_layout.model.layoutChanged.emit()
4✔
154
        self.installed_layout.view.clearSelection()
4✔
155
        self.available_layout.model.layoutChanged.emit()
4✔
156
        self.available_layout.view.clearSelection()
4✔
157
        self.com.on_languages_changed.emit(installed)
4✔
158

159
    def _get_installed_languages(self) -> list[str]:
4✔
160
        languages = [f.stem for f in self.tessdata_path.glob("*.traineddata")]
4✔
161
        return sorted(languages)
4✔
162

163
    def _set_in_progress(self, value: bool) -> None:
4✔
164
        self.available_layout.view.setEnabled(not value)
4✔
165
        self.available_layout.button.setEnabled(not value)
4✔
166
        self.installed_layout.view.setEnabled(not value)
4✔
167
        self.installed_layout.button.setEnabled(not value)
4✔
168
        self.tessdata_label.setEnabled(not value)
4✔
169
        self.loading_indicator.setVisible(value)
4✔
170

171

172
class IconLabel(QtWidgets.QWidget):
4✔
173
    """Label with icon in front."""
174

175
    def __init__(self, icon: str, text: str) -> None:
4✔
176
        super().__init__()
4✔
177

178
        layout = QtWidgets.QHBoxLayout()
4✔
179
        self.setLayout(layout)
4✔
180

181
        icon_label = QtWidgets.QLabel()
4✔
182
        pixmapi = getattr(QtWidgets.QStyle, icon)
4✔
183
        icon_label.setPixmap(self.style().standardIcon(pixmapi).pixmap(16, 16))
4✔
184

185
        layout.addWidget(icon_label)
4✔
186
        layout.addSpacing(2)
4✔
187
        layout.addWidget(QtWidgets.QLabel(text))
4✔
188
        layout.addStretch()
4✔
189

190

191
class MinimalTableView(QtWidgets.QTableView):
4✔
192
    """TableView without grid, headers.
193

194
    - Only allows selection of rows.
195
    - Focus is disabled to avoid cells being focused. (Didn't found a better workaround)
196
    """
197

198
    def __init__(self, model: QtCore.QAbstractTableModel) -> None:
4✔
199
        super().__init__()
4✔
200
        self.setShowGrid(False)
4✔
201
        self.horizontalHeader().setSectionResizeMode(
4✔
202
            QtWidgets.QHeaderView.ResizeMode.ResizeToContents
203
        )
204
        self.horizontalHeader().setVisible(False)
4✔
205
        self.verticalHeader().setVisible(False)
4✔
206
        self.setSelectionBehavior(
4✔
207
            QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows
208
        )
209
        self.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.SingleSelection)
4✔
210
        self.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus)
4✔
211
        self.setModel(model)
4✔
212

213

214
class LanguageLayout(QtWidgets.QVBoxLayout):
4✔
215
    """Layout includes: Label, TableView with Model, Button."""
216

217
    def __init__(
4✔
218
        self, label_text: str, label_icon: str, button_text: str, button_icon: str
219
    ) -> None:
220
        super().__init__()
4✔
221
        self.model = LanguageModel(parent=self)
4✔
222

223
        self.addWidget(IconLabel(icon=label_icon, text=label_text))
4✔
224

225
        self.view = MinimalTableView(model=self.model)
4✔
226
        self.addWidget(self.view)
4✔
227

228
        pixmap = getattr(
4✔
229
            QtWidgets.QStyle.StandardPixmap,
230
            button_icon,
231
            QtWidgets.QStyle.StandardPixmap.SP_DialogHelpButton,
232
        )
233

234
        button_qicon = QtWidgets.QApplication.style().standardIcon(pixmap)
4✔
235
        self.button = QtWidgets.QPushButton(button_qicon, button_text)
4✔
236
        self.addWidget(self.button)
4✔
237

238

239
class LanguageModel(QtCore.QAbstractTableModel):
4✔
240
    def __init__(
4✔
241
        self, parent: Optional[QtCore.QObject] = None, languages: Optional[list] = None
242
    ) -> None:
243
        super().__init__(parent=parent)
4✔
244
        self.languages: list = languages or []
4✔
245

246
    def data(
4✔
247
        self, index: QtCore.QModelIndex, role: QtCore.Qt.ItemDataRole
248
    ) -> Union[str, QtCore.QSize, None]:
UNCOV
249
        if role == QtCore.Qt.ItemDataRole.DisplayRole:
×
UNCOV
250
            return self.languages[index.row()][index.column()]
×
UNCOV
251
        return None
×
252

253
    def rowCount(self, index: QtCore.QModelIndex) -> int:  # noqa: N802
4✔
254
        return len(self.languages)
4✔
255

256
    def columnCount(self, index: QtCore.QModelIndex) -> int:  # noqa: N802
4✔
257
        return len(self.languages[0]) if self.languages else 0
4✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc