1# -*- coding: utf-8 -*- 2 3# Copyright (C) 2007 Osmo Salomaa 4# 5# This program is free software: you can redistribute it and/or modify 6# it under the terms of the GNU General Public License as published by 7# the Free Software Foundation, either version 3 of the License, or 8# (at your option) any later version. 9# 10# This program is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18"""Assistant to guide through multiple text correction tasks.""" 19 20import aeidon 21import gaupol 22import os 23 24from aeidon.i18n import _, n_ 25from gi.repository import Gdk 26from gi.repository import GLib 27from gi.repository import GObject 28from gi.repository import Gtk 29from gi.repository import Pango 30 31__all__ = ("TextAssistant", "TextAssistantPage") 32 33 34class TextAssistantPage(Gtk.Box): 35 36 """ 37 Baseclass for pages of :class:`TextAssistant`. 38 39 :ivar description: One-line description used in the introduction page 40 :ivar handle: Unique unlocalized name for internal references 41 :ivar page_title: Short string used as configuration page title 42 :ivar page_type: A :class:`Gtk.AssistantPageType` constant 43 :ivar title: Short title used in the introduction page 44 45 Of these attributes, :attr:`description`, :attr:`handle` and :attr:`title` 46 are only required for pages of type :attr:`Gtk.AssistantPageType.CONTENT`. 47 """ 48 49 def __init__(self, assistant): 50 """Initialize a :class:`TextAssistantPage` instance.""" 51 GObject.GObject.__init__(self, orientation=Gtk.Orientation.VERTICAL) 52 self.assistant = assistant 53 self.description = None 54 self.handle = None 55 self.page_title = None 56 self.page_type = None 57 self.title = None 58 59 60class BuilderPage(TextAssistantPage): 61 62 """Baseclass for pages of :class:`TextAssistant` built with GtkBuilder.""" 63 64 _widgets = () 65 66 def __init__(self, assistant, basename): 67 """Initialize a :class:`BuilderPage` instance.""" 68 TextAssistantPage.__init__(self, assistant) 69 self._builder = Gtk.Builder() 70 self._builder.set_translation_domain("gaupol") 71 self._builder.add_from_file(os.path.join( 72 aeidon.DATA_DIR, "ui", "text-assistant", basename)) 73 self._builder.connect_signals(self) 74 self._set_attributes(self._widgets) 75 container = self._builder.get_object("main_container") 76 window = container.get_parent() 77 window.remove(container) 78 self.add(container) 79 gaupol.util.idle_add(window.destroy) 80 81 def _set_attributes(self, widgets): 82 """Assign all names in `widgets` as attributes of `self`.""" 83 for name in widgets: 84 widget = self._builder.get_object(name) 85 setattr(self, "_{}".format(name), widget) 86 87 88class IntroductionPage(BuilderPage): 89 90 """Page for listing all text correction tasks.""" 91 92 _widgets = ("columns_combo", "subtitles_combo", "tree_view") 93 94 def __init__(self, assistant): 95 """Initialize a :class:`IntroductionPage` instance.""" 96 BuilderPage.__init__(self, assistant, "introduction-page.ui") 97 # TRANSLATORS: Keep these page titles short, since they 98 # affect the width of the text correction assistant sidebar. 99 self.page_title = _("Tasks and Target") 100 self.page_type = Gtk.AssistantPageType.INTRO 101 self._init_columns_combo() 102 self._init_subtitles_combo() 103 self._init_tree_view() 104 self._init_values() 105 106 def get_field(self): 107 """Return the selected field.""" 108 index = self._columns_combo.get_active() 109 return [gaupol.fields.MAIN_TEXT, 110 gaupol.fields.TRAN_TEXT][index] 111 112 def get_selected_pages(self): 113 """Return selected content pages.""" 114 store = self._tree_view.get_model() 115 return [x[0] for x in store if x[1]] 116 117 def get_target(self): 118 """Return the selected target.""" 119 index = self._subtitles_combo.get_active() 120 return [gaupol.targets.SELECTED, 121 gaupol.targets.CURRENT, 122 gaupol.targets.ALL][index] 123 124 def _init_columns_combo(self): 125 """Initalize the columns target combo box.""" 126 store = Gtk.ListStore(str) 127 self._columns_combo.set_model(store) 128 store.append((_("Correct texts in the text column"),)) 129 store.append((_("Correct texts in the translation column"),)) 130 renderer = Gtk.CellRendererText() 131 self._columns_combo.pack_start(renderer, expand=True) 132 self._columns_combo.add_attribute(renderer, "text", 0) 133 134 def _init_subtitles_combo(self): 135 """Initalize the subtitles target combo box.""" 136 store = Gtk.ListStore(str) 137 self._subtitles_combo.set_model(store) 138 store.append((_("Correct texts in selected subtitles"),)) 139 store.append((_("Correct texts in current project"),)) 140 store.append((_("Correct texts in all open projects"),)) 141 renderer = Gtk.CellRendererText() 142 self._subtitles_combo.pack_start(renderer, expand=True) 143 self._subtitles_combo.add_attribute(renderer, "text", 0) 144 145 def _init_tree_view(self): 146 """Initialize the tree view of tasks.""" 147 store = Gtk.ListStore(object, bool, str) 148 self._tree_view.set_model(store) 149 selection = self._tree_view.get_selection() 150 selection.set_mode(Gtk.SelectionMode.SINGLE) 151 renderer = Gtk.CellRendererToggle() 152 renderer.props.activatable = True 153 renderer.props.xpad = 6 154 renderer.connect("toggled", self._on_tree_view_cell_toggled) 155 column = Gtk.TreeViewColumn("", renderer, active=1) 156 self._tree_view.append_column(column) 157 renderer = Gtk.CellRendererText() 158 renderer.props.ellipsize = Pango.EllipsizeMode.END 159 column = Gtk.TreeViewColumn("", renderer, markup=2) 160 self._tree_view.append_column(column) 161 162 def _init_values(self): 163 """Initialize default values for widgets.""" 164 self._columns_combo.set_active({ 165 gaupol.fields.MAIN_TEXT: 0, 166 gaupol.fields.TRAN_TEXT: 1, 167 }[gaupol.conf.text_assistant.field]) 168 self._subtitles_combo.set_active({ 169 gaupol.targets.SELECTED: 0, 170 gaupol.targets.CURRENT: 1, 171 gaupol.targets.ALL: 2, 172 }[gaupol.conf.text_assistant.target]) 173 174 def _on_columns_combo_changed(self, *args): 175 """Save the selected field.""" 176 gaupol.conf.text_assistant.field = self.get_field() 177 178 def _on_subtitles_combo_changed(self, *args): 179 """Save the selected target.""" 180 gaupol.conf.text_assistant.target = self.get_target() 181 182 def _on_tree_view_cell_toggled(self, renderer, path): 183 """Toggle and save task check button value.""" 184 store = self._tree_view.get_model() 185 store[path][1] = not store[path][1] 186 store[path][0].set_visible(store[path][1]) 187 pages = [x.handle for x in self.get_selected_pages()] 188 gaupol.conf.text_assistant.pages = pages 189 190 def populate_tree_view(self, content_pages): 191 """Populate the tree view with tasks from `content_pages`.""" 192 self._tree_view.get_model().clear() 193 store = self._tree_view.get_model() 194 pages = gaupol.conf.text_assistant.pages 195 for page in content_pages: 196 title = GLib.markup_escape_text(page.title) 197 description = GLib.markup_escape_text(page.description) 198 markup = "<b>{}</b>\n{}".format(title, description) 199 page.set_visible(page.handle in pages) 200 store.append((page, page.handle in pages, markup)) 201 self._tree_view.get_selection().unselect_all() 202 203 204class LocalePage(BuilderPage): 205 206 """Page with script, language and coutry based pattern selection.""" 207 208 _ui_file_basename = NotImplementedError 209 210 _widgets = ( 211 "country_combo", 212 "country_label", 213 "language_combo", 214 "language_label", 215 "script_combo", 216 "script_label", 217 "tree_view", 218 ) 219 220 def __init__(self, assistant): 221 """Initialize a :class:`LocalePage` instance.""" 222 BuilderPage.__init__(self, assistant, self._ui_file_basename) 223 self.conf = None 224 self._init_attributes() 225 self._init_tree_view() 226 self._init_combo_boxes() 227 self._init_values() 228 229 def correct_texts(self, project, indices, doc): 230 """Correct texts in `project`.""" 231 raise NotImplementedError 232 233 def _filter_patterns(self, patterns): 234 """Return a subset of `patterns` to show.""" 235 return patterns 236 237 def _get_country(self): 238 """Return the selected country or ``None``.""" 239 if not self._country_combo.get_sensitive(): return None 240 index = self._country_combo.get_active() 241 if index < 0: return None 242 store = self._country_combo.get_model() 243 value = store[index][0] 244 return (None if value == "other" else value) 245 246 def _get_language(self): 247 """Return the selected language or ``None``.""" 248 if not self._language_combo.get_sensitive(): return None 249 index = self._language_combo.get_active() 250 if index < 0: return None 251 store = self._language_combo.get_model() 252 value = store[index][0] 253 return (None if value == "other" else value) 254 255 def _get_script(self): 256 """Return the selected script or ``None``.""" 257 if not self._script_combo.get_sensitive(): return None 258 index = self._script_combo.get_active() 259 if index < 0: return None 260 store = self._script_combo.get_model() 261 value = store[index][0] 262 return (None if value == "other" else value) 263 264 def _init_attributes(self): 265 """Initialize values of page attributes.""" 266 raise NotImplementedError 267 268 def _init_combo(self, combo_box): 269 """Initialize `combo_box` and populate with `items`.""" 270 store = Gtk.ListStore(str, str) 271 combo_box.set_model(store) 272 renderer = Gtk.CellRendererText() 273 combo_box.pack_start(renderer, expand=True) 274 combo_box.add_attribute(renderer, "text", 1) 275 func = gaupol.util.separate_combo 276 combo_box.set_row_separator_func(func, None) 277 278 def _init_combo_boxes(self): 279 """Initialize and populate combo boxes.""" 280 self._init_combo(self._script_combo) 281 self._init_combo(self._language_combo) 282 self._init_combo(self._country_combo) 283 self._populate_script_combo() 284 self._populate_language_combo() 285 self._populate_country_combo() 286 287 def _init_tree_view(self): 288 """Initialize the tree view of individual corrections.""" 289 store = Gtk.ListStore(object, bool, bool, str) 290 store_filter = store.filter_new() 291 store_filter.set_visible_column(1) 292 self._tree_view.set_model(store_filter) 293 selection = self._tree_view.get_selection() 294 selection.set_mode(Gtk.SelectionMode.SINGLE) 295 renderer = Gtk.CellRendererToggle() 296 renderer.props.activatable = True 297 renderer.props.xpad = 6 298 renderer.connect("toggled", self._on_tree_view_cell_toggled) 299 column = Gtk.TreeViewColumn("", renderer, active=2) 300 self._tree_view.append_column(column) 301 renderer = Gtk.CellRendererText() 302 renderer.props.ellipsize = Pango.EllipsizeMode.END 303 column = Gtk.TreeViewColumn("", renderer, markup=3) 304 self._tree_view.append_column(column) 305 306 def _init_values(self): 307 """Initialize default values for widgets.""" 308 pass 309 310 def _on_country_combo_changed(self, combo_box): 311 """Populate the tree view with a subset patterns.""" 312 self.conf.country = self._get_country() or "" 313 self._populate_tree_view() 314 315 def _on_language_combo_changed(self, combo_box): 316 """Populate the tree view with a subset patterns.""" 317 language = self._get_language() 318 sensitive = language is not None 319 self._populate_country_combo() 320 self._country_combo.set_sensitive(sensitive) 321 self._country_label.set_sensitive(sensitive) 322 self.conf.language = language or "" 323 self._populate_tree_view() 324 325 def _on_script_combo_changed(self, combo_box): 326 """Populate the tree view with a subset patterns.""" 327 script = self._get_script() 328 sensitive = script is not None 329 self._populate_language_combo() 330 self._language_combo.set_sensitive(sensitive) 331 self._language_label.set_sensitive(sensitive) 332 language = self._get_language() 333 sensitive = sensitive and language is not None 334 self._populate_country_combo() 335 self._country_combo.set_sensitive(sensitive) 336 self._country_label.set_sensitive(sensitive) 337 self.conf.script = script or "" 338 self._populate_tree_view() 339 340 def _on_tree_view_cell_toggled(self, renderer, path): 341 """Toggle the check button value.""" 342 store_filter = self._tree_view.get_model() 343 store = store_filter.get_model() 344 path = Gtk.TreePath.new_from_string(path) 345 path = store_filter.convert_path_to_child_path(path) 346 name = store[path][0].get_name(False) 347 enabled = not store[path][2] 348 for i in range(len(store)): 349 # Toggle all patterns with the same name. 350 if store[i][0].get_name(False) == name: 351 store[i][0].enabled = enabled 352 store[i][2] = enabled 353 354 def _populate_combo(self, combo_box, items, active): 355 """Populate `combo_box` with `items`.""" 356 store = combo_box.get_model() 357 combo_box.set_model(None) 358 store.clear() 359 for code, name in items: 360 store.append((code, name)) 361 if len(store) > 0: 362 store.append((gaupol.COMBO_SEPARATOR, "")) 363 store.append(("other", _("Other"))) 364 combo_box.set_active(len(store) - 1) 365 for i in range(len(store)): 366 if store[i][0] == active and active: 367 combo_box.set_active(i) 368 combo_box.set_model(store) 369 370 def _populate_country_combo(self): 371 """Populate the country combo box.""" 372 script = self._get_script() 373 language = self._get_language() 374 codes = self._manager.get_countries(script, language) 375 names = list(map(aeidon.countries.code_to_name, codes)) 376 items = [(codes[i], names[i]) for i in range(len(codes))] 377 items.sort(key=lambda x: x[1]) 378 self._populate_combo(self._country_combo, items, self.conf.country) 379 380 def _populate_language_combo(self): 381 """Populate the language combo box.""" 382 script = self._get_script() 383 codes = self._manager.get_languages(script) 384 names = list(map(aeidon.languages.code_to_name, codes)) 385 items = [(codes[i], names[i]) for i in range(len(codes))] 386 items.sort(key=lambda x: x[1]) 387 self._populate_combo(self._language_combo, items, self.conf.language) 388 389 def _populate_script_combo(self): 390 """Populate the script combo box.""" 391 codes = self._manager.get_scripts() 392 names = list(map(aeidon.scripts.code_to_name, codes)) 393 items = [(codes[i], names[i]) for i in range(len(codes))] 394 items.sort(key=lambda x: x[1]) 395 self._populate_combo(self._script_combo, items, self.conf.script) 396 397 def _populate_tree_view(self): 398 """Populate the tree view with a subset patterns.""" 399 store_filter = self._tree_view.get_model() 400 store = store_filter.get_model() 401 store.clear() 402 script = self._get_script() 403 language = self._get_language() 404 country = self._get_country() 405 patterns = self._manager.get_patterns(script, language, country) 406 patterns = self._filter_patterns(patterns) 407 names_entered = set(()) 408 for pattern in patterns: 409 name = pattern.get_name() 410 visible = not name in names_entered 411 names_entered.add(name) 412 name = GLib.markup_escape_text(name) 413 description = pattern.get_description() 414 description = GLib.markup_escape_text(description) 415 markup = "<b>{}</b>\n{}".format(name, description) 416 store.append((pattern, visible, pattern.enabled, markup)) 417 self._tree_view.get_selection().unselect_all() 418 419 420class CapitalizationPage(LocalePage): 421 422 """Page for capitalizing texts in subtitles.""" 423 424 _ui_file_basename = "capitalization-page.ui" 425 426 def correct_texts(self, project, indices, doc): 427 """Correct texts in `project`.""" 428 script = self._get_script() 429 language = self._get_language() 430 country = self._get_country() 431 self._manager.save_config(script, language, country) 432 patterns = self._manager.get_patterns(script, language, country) 433 project.capitalize(indices, doc, patterns) 434 435 def _init_attributes(self): 436 """Initialize values of page attributes.""" 437 self._manager = aeidon.PatternManager("capitalization") 438 self.conf = gaupol.conf.capitalization 439 self.description = _("Capitalize texts written in lower case") 440 self.handle = "capitalization" 441 # TRANSLATORS: Keep these page titles short, since they 442 # affect the width of the text correction assistant sidebar. 443 self.page_title = _("Capitalization Patterns") 444 self.page_type = Gtk.AssistantPageType.CONTENT 445 self.title = _("Capitalize texts") 446 447 448class CommonErrorPage(LocalePage): 449 450 """Page for correcting common human and OCR errors.""" 451 452 _ui_file_basename = "common-error-page.ui" 453 _widgets = ("human_check", "ocr_check") + LocalePage._widgets 454 455 def correct_texts(self, project, indices, doc): 456 """Correct texts in `project`.""" 457 script = self._get_script() 458 language = self._get_language() 459 country = self._get_country() 460 self._manager.save_config(script, language, country) 461 patterns = self._manager.get_patterns(script, language, country) 462 project.correct_common_errors(indices, doc, patterns) 463 464 def _init_attributes(self): 465 """Initialize values of page attributes.""" 466 self._manager = aeidon.PatternManager("common-error") 467 self.conf = gaupol.conf.common_error 468 self.description = _("Correct common errors made by humans or image recognition software") 469 self.handle = "common-error" 470 # TRANSLATORS: Keep these page titles short, since they 471 # affect the width of the text correction assistant sidebar. 472 self.page_title = _("Common Error Patterns") 473 self.page_type = Gtk.AssistantPageType.CONTENT 474 self.title = _("Correct common errors") 475 476 def _filter_patterns(self, patterns): 477 """Return a subset of `patterns` to show.""" 478 def use_pattern(pattern): 479 classes = set(pattern.get_field_list("Classes")) 480 return(bool(classes & set(self.conf.classes))) 481 return list(filter(use_pattern, patterns)) 482 483 def _init_values(self): 484 """Initialize default values for widgets.""" 485 self._human_check.set_active("Human" in self.conf.classes) 486 self._ocr_check.set_active("OCR" in self.conf.classes) 487 488 def _on_human_check_toggled(self, check_button): 489 """Populate the tree view with a subset patterns.""" 490 if check_button.get_active(): 491 self.conf.classes.append("Human") 492 self.conf.classes = sorted(set(self.conf.classes)) 493 elif "Human" in self.conf.classes: 494 self.conf.classes.remove("Human") 495 self._populate_tree_view() 496 497 def _on_ocr_check_toggled(self, check_button): 498 """Populate the tree view with a subset patterns.""" 499 if check_button.get_active(): 500 self.conf.classes.append("OCR") 501 self.conf.classes = sorted(set(self.conf.classes)) 502 elif "OCR" in self.conf.classes: 503 self.conf.classes.remove("OCR") 504 self._populate_tree_view() 505 506 507class HearingImpairedPage(LocalePage): 508 509 """Page for removing hearing impaired parts from subtitles.""" 510 511 _ui_file_basename = "hearing-impaired-page.ui" 512 513 def correct_texts(self, project, indices, doc): 514 """Correct texts in `project`.""" 515 script = self._get_script() 516 language = self._get_language() 517 country = self._get_country() 518 self._manager.save_config(script, language, country) 519 patterns = self._manager.get_patterns(script, language, country) 520 project.remove_hearing_impaired(indices, doc, patterns) 521 522 def _init_attributes(self): 523 """Initialize values of page attributes.""" 524 self._manager = aeidon.PatternManager("hearing-impaired") 525 self.conf = gaupol.conf.hearing_impaired 526 self.description = _("Remove explanatory texts meant for the hearing impaired") 527 self.handle = "hearing-impaired" 528 # TRANSLATORS: Keep these page titles short, since they 529 # affect the width of the text correction assistant sidebar. 530 self.page_title = _("Hearing Impaired Patterns") 531 self.page_type = Gtk.AssistantPageType.CONTENT 532 self.title = _("Remove hearing impaired texts") 533 534 535class JoinSplitWordsPage(BuilderPage): 536 537 """Page for joining or splitting words based on spell-check suggestions.""" 538 539 _widgets = ("language_button", "join_check", "split_check") 540 541 def __init__(self, assistant): 542 """Initialize a :class:`JoinSplitWordsPage` instance.""" 543 BuilderPage.__init__(self, assistant, "join-split-page.ui") 544 self.description = _("Use spell-check suggestions to fix whitespace detection errors of image recognition software") 545 self.handle = "join-split-words" 546 # TRANSLATORS: Keep these page titles short, since they 547 # affect the width of the text correction assistant sidebar. 548 self.page_title = _("Joining and Splitting Words") 549 self.page_type = Gtk.AssistantPageType.CONTENT 550 self.title = _("Join or Split Words") 551 self._init_values() 552 553 def correct_texts(self, project, indices, doc): 554 """Correct texts in `project`.""" 555 import enchant 556 language = gaupol.conf.spell_check.language 557 if gaupol.conf.join_split_words.join: 558 try: 559 project.spell_check_join_words(indices, doc, language) 560 except enchant.Error as error: 561 return self._show_error_dialog(str(error)) 562 if gaupol.conf.join_split_words.split: 563 try: 564 project.spell_check_split_words(indices, doc, language) 565 except enchant.Error as error: 566 return self._show_error_dialog(str(error)) 567 568 def _init_values(self): 569 """Initialize default values for widgets.""" 570 language = gaupol.conf.spell_check.language 571 language = aeidon.locales.code_to_name(language) 572 self._language_button.set_label(language) 573 self._join_check.set_active(gaupol.conf.join_split_words.join) 574 self._split_check.set_active(gaupol.conf.join_split_words.split) 575 576 def _on_join_check_toggled(self, check_button, *args): 577 """Save value of join option.""" 578 gaupol.conf.join_split_words.join = check_button.get_active() 579 580 def _on_language_button_clicked(self, button, *args): 581 """Show a language dialog and update `button` label.""" 582 gaupol.util.set_cursor_busy(self.assistant) 583 dialog = gaupol.LanguageDialog(self.assistant, False) 584 gaupol.util.set_cursor_normal(self.assistant) 585 gaupol.util.flash_dialog(dialog) 586 language = gaupol.conf.spell_check.language 587 language = aeidon.locales.code_to_name(language) 588 self._language_button.set_label(language) 589 590 def _on_split_check_toggled(self, check_button, *args): 591 """Save value of split option.""" 592 gaupol.conf.join_split_words.split = check_button.get_active() 593 594 def _show_error_dialog(self, message): 595 """Show an error dialog after failing to load dictionary.""" 596 name = gaupol.conf.spell_check.language 597 name = aeidon.locales.code_to_name(name) 598 title = _('Failed to load dictionary for language "{}"').format(name) 599 dialog = gaupol.ErrorDialog(self.get_parent(), title, message) 600 dialog.add_button(_("_OK"), Gtk.ResponseType.OK) 601 dialog.set_default_response(Gtk.ResponseType.OK) 602 gaupol.util.flash_dialog(dialog) 603 604 605class LineBreakPage(LocalePage): 606 607 """Page for breaking text into lines.""" 608 609 _ui_file_basename = "line-break-page.ui" 610 611 def correct_texts(self, project, indices, doc): 612 """Correct texts in `project`.""" 613 script = self._get_script() 614 language = self._get_language() 615 country = self._get_country() 616 self._manager.save_config(script, language, country) 617 patterns = self._manager.get_patterns(script, language, country) 618 length_func = gaupol.ruler.get_length_function(self.conf.length_unit) 619 skip = self.conf.use_skip_max_length or self.conf.use_skip_max_lines 620 project.break_lines(indices=indices, 621 doc=doc, 622 patterns=patterns, 623 length_func=length_func, 624 max_length=self.conf.max_length, 625 max_lines=self.conf.max_lines, 626 skip=skip, 627 max_skip_length=self._max_skip_length, 628 max_skip_lines=self._max_skip_lines) 629 630 def _init_attributes(self): 631 """Initialize values of page attributes.""" 632 self._manager = aeidon.PatternManager("line-break") 633 self.conf = gaupol.conf.line_break 634 self.description = _("Break text into lines of defined length") 635 self.handle = "line-break" 636 # TRANSLATORS: Keep these page titles short, since they 637 # affect the width of the text correction assistant sidebar. 638 self.page_title = _("Line-Break Patterns") 639 self.page_type = Gtk.AssistantPageType.CONTENT 640 self.title = _("Break lines") 641 642 @property 643 def _max_skip_length(self): 644 """Return the maximum line length to skip.""" 645 if self.conf.use_skip_max_length: 646 return self.conf.skip_max_length 647 return 32768 648 649 @property 650 def _max_skip_lines(self): 651 """Return the maximum amount of lines to skip.""" 652 if self.conf.use_skip_max_lines: 653 return self.conf.skip_max_lines 654 return 32768 655 656 657class LineBreakOptionsPage(BuilderPage): 658 659 """Page for editing line-break options.""" 660 661 _widgets = ( 662 "max_length_spin", 663 "max_lines_spin", 664 "max_skip_length_spin", 665 "max_skip_lines_spin", 666 "skip_length_check", 667 "skip_lines_check", 668 "skip_unit_combo", 669 "unit_combo", 670 ) 671 672 def __init__(self, assistant): 673 """Initialize a :class:`LineBreakOptionsPage` instance.""" 674 BuilderPage.__init__(self, assistant, "line-break-options-page.ui") 675 self.conf = gaupol.conf.line_break 676 # TRANSLATORS: Keep these page titles short, since they 677 # affect the width of the text correction assistant sidebar. 678 self.page_title = _("Line-Break Options") 679 self.page_type = Gtk.AssistantPageType.CONTENT 680 self._init_unit_combo(self._unit_combo) 681 self._init_unit_combo(self._skip_unit_combo) 682 self._init_values() 683 684 def _init_unit_combo(self, combo_box): 685 """Initialize line length unit `combo_box`.""" 686 store = Gtk.ListStore(str) 687 combo_box.set_model(store) 688 for label in (x.label for x in gaupol.length_units): 689 store.append((label,)) 690 renderer = Gtk.CellRendererText() 691 combo_box.pack_start(renderer, expand=True) 692 combo_box.add_attribute(renderer, "text", 0) 693 694 def _init_values(self): 695 """Initialize default values for widgets.""" 696 self._max_length_spin.set_value(self.conf.max_length) 697 self._max_lines_spin.set_value(self.conf.max_lines) 698 self._max_skip_length_spin.set_value(self.conf.skip_max_length) 699 self._max_skip_lines_spin.set_value(self.conf.skip_max_lines) 700 self._skip_length_check.set_active(self.conf.use_skip_max_length) 701 self._skip_lines_check.set_active(self.conf.use_skip_max_lines) 702 self._skip_unit_combo.set_active(self.conf.length_unit) 703 self._unit_combo.set_active(self.conf.length_unit) 704 705 def _on_max_length_spin_value_changed(self, spin_button): 706 """Save maximum line length value.""" 707 self.conf.max_length = spin_button.get_value_as_int() 708 709 def _on_max_lines_spin_value_changed(self, spin_button): 710 """Save maximum line amount value.""" 711 self.conf.max_lines = spin_button.get_value_as_int() 712 713 def _on_max_skip_length_spin_value_changed(self, spin_button): 714 """Save maximum line length to skip value.""" 715 self.conf.skip_max_length = spin_button.get_value_as_int() 716 717 def _on_max_skip_lines_spin_value_changed(self, spin_button): 718 """Save maximum line amount to skip value.""" 719 self.conf.skip_max_lines = spin_button.get_value_as_int() 720 721 def _on_skip_length_check_toggled(self, check_button): 722 """Save skip by line length value.""" 723 use_skip = check_button.get_active() 724 self.conf.use_skip_max_length = use_skip 725 self._max_skip_length_spin.set_sensitive(use_skip) 726 self._skip_unit_combo.set_sensitive(use_skip) 727 728 def _on_skip_lines_check_toggled(self, check_button): 729 """Save skip by line amount value.""" 730 use_skip = check_button.get_active() 731 self.conf.use_skip_max_lines = use_skip 732 self._max_skip_lines_spin.set_sensitive(use_skip) 733 734 def _on_skip_unit_combo_changed(self, combo_box): 735 """Save and sync length unit value of `combo_box.""" 736 index = combo_box.get_active() 737 length_unit = gaupol.length_units[index] 738 self.conf.length_unit = length_unit 739 self._unit_combo.set_active(index) 740 741 def _on_unit_combo_changed(self, combo_box): 742 """Save and sync length unit value of `combo_box.""" 743 index = combo_box.get_active() 744 length_unit = gaupol.length_units[index] 745 self.conf.length_unit = length_unit 746 self._skip_unit_combo.set_active(index) 747 748 749class ProgressPage(BuilderPage): 750 751 """Page for showing progress of text corrections.""" 752 753 _widgets = ("message_label", "progress_bar", "status_label", "task_label") 754 755 def __init__(self, assistant): 756 """Initialize a :class:`ProgressPage` instance.""" 757 BuilderPage.__init__(self, assistant, "progress-page.ui") 758 self._current_task = None 759 self._total_tasks = None 760 # TRANSLATORS: Keep these page titles short, since they 761 # affect the width of the text correction assistant sidebar. 762 self.page_title = _("Correcting Texts") 763 self.page_type = Gtk.AssistantPageType.PROGRESS 764 self._init_values() 765 766 def _init_values(self): 767 """Initalize default values for widgets.""" 768 message = _("Each task is now being run on each project.") 769 self._message_label.set_text(message) 770 self.reset(100) 771 772 def bump_progress(self, n=1): 773 """Bump the current progress by `n`.""" 774 self.set_progress(self._current_task + n) 775 776 def reset(self, total, clear_text=False): 777 """Set `total` as the amount of tasks to be run.""" 778 self._current_task = 0 779 self._total_tasks = total 780 self.set_progress(0, total) 781 self.set_project_name("") 782 self.set_task_name("") 783 if clear_text: 784 self._progress_bar.set_text("") 785 gaupol.util.iterate_main() 786 787 def set_progress(self, current, total=None): 788 """Set current as the task progress status.""" 789 total = total or self._total_tasks 790 fraction = (current/total if total > 0 else 0) 791 self._progress_bar.set_fraction(fraction) 792 text = _("{current:d} of {total:d} tasks complete") 793 self._progress_bar.set_text(text.format(**locals())) 794 self._current_task = current 795 self._total_tasks = total 796 gaupol.util.iterate_main() 797 798 def set_project_name(self, name): 799 """Set `name` as the currently checked project.""" 800 text = _("Project: {}").format(name) 801 self._status_label.set_text(text) 802 gaupol.util.iterate_main() 803 804 def set_task_name(self, name): 805 """Set `name` as the currently performed task.""" 806 text = _("Task: {}").format(name) 807 self._task_label.set_text(text) 808 gaupol.util.iterate_main() 809 810 811class ConfirmationPage(BuilderPage): 812 813 """Page to confirm changes made after performing all tasks.""" 814 815 _widgets = ( 816 "mark_all_button", 817 "preview_button", 818 "remove_check", 819 "tree_view", 820 "unmark_all_button", 821 ) 822 823 def __init__(self, assistant): 824 """Initialize a :class:`ConfirmationPage` instance.""" 825 BuilderPage.__init__(self, assistant, "confirmation-page.ui") 826 self.application = None 827 self.conf = gaupol.conf.text_assistant 828 self.doc = None 829 # TRANSLATORS: Keep these page titles short, since they 830 # affect the width of the text correction assistant sidebar. 831 self.page_title = _("Confirm Changes") 832 self.page_type = Gtk.AssistantPageType.CONFIRM 833 self._init_tree_view() 834 self._init_values() 835 836 def _add_text_column(self, index, title): 837 """Add a multiline text column to the tree view.""" 838 renderer = gaupol.MultilineCellRenderer() 839 renderer.set_show_lengths(True) 840 renderer.props.editable = (index == 4) 841 renderer.props.ellipsize = Pango.EllipsizeMode.END 842 renderer.props.font = gaupol.util.get_font() 843 renderer.props.yalign = 0 844 renderer.props.xpad = 4 845 renderer.props.ypad = 4 846 column = Gtk.TreeViewColumn(title, renderer, text=index) 847 column.set_resizable(True) 848 column.set_expand(True) 849 self._tree_view.append_column(column) 850 851 def _can_preview(self): 852 """Return ``True`` if preview is possible.""" 853 row = self._get_selected_row() 854 if row is None: return False 855 store = self._tree_view.get_model() 856 page = store[row][0] 857 if page is None: return False 858 return bool(page.project.video_path and page.project.main_file) 859 860 def get_confirmed_changes(self): 861 """Return a sequence of changes marked as accepted.""" 862 changes = [] 863 store = self._tree_view.get_model() 864 for row in (x for x in store if x[2]): 865 page, index, accept, orig, new = row 866 changes.append((page, index, orig, new)) 867 return tuple(changes) 868 869 def _get_selected_row(self): 870 """Return the selected row in the tree view or ``None``.""" 871 selection = self._tree_view.get_selection() 872 store, itr = selection.get_selected() 873 if itr is None: return None 874 path = store.get_path(itr) 875 return gaupol.util.tree_path_to_row(path) 876 877 def _init_tree_view(self): 878 """Initialize the tree view of corrections.""" 879 # page, index, accept, original text, new text 880 store = Gtk.ListStore(object, int, bool, str, str) 881 self._tree_view.set_model(store) 882 selection = self._tree_view.get_selection() 883 selection.set_mode(Gtk.SelectionMode.SINGLE) 884 selection.connect("changed", self._on_tree_view_selection_changed) 885 renderer = Gtk.CellRendererToggle() 886 renderer.props.activatable = True 887 renderer.props.xpad = 6 888 renderer.connect("toggled", self._on_tree_view_cell_toggled) 889 column = Gtk.TreeViewColumn(_("Accept"), renderer, active=2) 890 column.set_resizable(True) 891 self._tree_view.append_column(column) 892 if gaupol.conf.editor.use_zebra_stripes: 893 callback = self._on_renderer_set_background 894 column.set_cell_data_func(renderer, callback, None) 895 self._add_text_column(3, _("Original Text")) 896 self._add_text_column(4, _("Corrected Text")) 897 column = self._tree_view.get_column(2) 898 renderer = column.get_cells()[0] 899 renderer.connect("edited", self._on_tree_view_cell_edited) 900 901 def _init_values(self): 902 """Initialize default values for widgets.""" 903 self._remove_check.set_active(self.conf.remove_blank) 904 self._preview_button.set_sensitive(False) 905 906 def _on_mark_all_button_clicked(self, *args): 907 """Set all accept column values to ``True``.""" 908 store = self._tree_view.get_model() 909 for i in range(len(store)): 910 store[i][2] = True 911 912 def _on_preview_button_clicked(self, *args): 913 """Preview original text in a video player.""" 914 row = self._get_selected_row() 915 store = self._tree_view.get_model() 916 page = store[row][0] 917 index = store[row][1] 918 position = page.project.subtitles[index].start 919 self.application.preview(page, position, self.doc) 920 921 def _on_remove_check_toggled(self, check_button): 922 """Save remove blank subtitles value.""" 923 self.conf.remove_blank = check_button.get_active() 924 925 def _on_renderer_set_background(self, column, renderer, store, itr, data): 926 """Set zerba-striped backgrounds for all columns.""" 927 path = self._tree_view.get_model().get_path(itr) 928 row = gaupol.util.tree_path_to_row(path) 929 color = (gaupol.util.get_zebra_color(self._tree_view) 930 if row % 2 == 0 else None) 931 932 for column in self._tree_view.get_columns(): 933 for renderer in column.get_cells(): 934 renderer.props.cell_background_rgba = color 935 936 def _on_tree_view_cell_edited(self, renderer, path, text): 937 """Edit text in the corrected text column.""" 938 store = self._tree_view.get_model() 939 store[path][4] = text 940 941 def _on_tree_view_cell_toggled(self, renderer, path): 942 """Toggle accept column value.""" 943 store = self._tree_view.get_model() 944 store[path][2] = not store[path][2] 945 946 def _on_tree_view_selection_changed(self, *args): 947 """Update preview button sensitivity.""" 948 self._preview_button.set_sensitive(self._can_preview()) 949 950 def _on_unmark_all_button_clicked(self, *args): 951 """Set all accept column values to ``False``.""" 952 store = self._tree_view.get_model() 953 for i in range(len(store)): 954 store[i][2] = False 955 956 def populate_tree_view(self, changes): 957 """Populate the tree view of changes to texts.""" 958 self._tree_view.get_model().clear() 959 store = self._tree_view.get_model() 960 for page, index, orig, new in changes: 961 store.append((page, index, True, orig, new)) 962 self._tree_view.get_selection().unselect_all() 963 964 965class TextAssistant(Gtk.Assistant): 966 967 """Assistant to guide through multiple text correction tasks.""" 968 969 def __init__(self, parent, application): 970 """Initialize a :class:`TextAssistant` instance.""" 971 GObject.GObject.__init__(self) 972 self._confirmation_page = ConfirmationPage(self) 973 self._introduction_page = IntroductionPage(self) 974 self._previous_page = None 975 self._progress_page = ProgressPage(self) 976 self.application = application 977 self._init_properties() 978 self._init_signal_handlers() 979 self.resize(*gaupol.conf.text_assistant.size) 980 if gaupol.conf.text_assistant.maximized: 981 self.maximize() 982 self.set_modal(True) 983 self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT) 984 self.set_transient_for(parent) 985 986 def add_page(self, page): 987 """Add `page` and configure its properties.""" 988 page.show_all() 989 self.append_page(page) 990 self.set_page_type(page, page.page_type) 991 self.set_page_title(page, page.page_title) 992 if page.page_type != Gtk.AssistantPageType.PROGRESS: 993 self.set_page_complete(page, True) 994 995 def add_pages(self, pages): 996 """Add associated `pages` and configure their properties.""" 997 for page in pages: 998 self.add_page(page) 999 def on_notify_visible(page, prop, pages): 1000 for page in pages[1:]: 1001 page.set_visible(pages[0].get_visible()) 1002 pages[0].connect("notify::visible", on_notify_visible, pages) 1003 1004 def _copy_project(self, project): 1005 """Return a copy of `project` with some same properties.""" 1006 copy = aeidon.Project(project.framerate) 1007 copy.main_file = project.main_file 1008 copy.tran_file = project.tran_file 1009 copy.subtitles = [x.copy() for x in project.subtitles] 1010 return copy 1011 1012 def _correct_texts(self, assistant_pages): 1013 """Correct texts by all pages and present changes.""" 1014 changes = [] 1015 target = self._introduction_page.get_target() 1016 field = self._introduction_page.get_field() 1017 doc = gaupol.util.text_field_to_document(field) 1018 rows = self.application.get_target_rows(target) 1019 application_pages = self.application.get_target_pages(target) 1020 total = len(application_pages) * len(assistant_pages) 1021 self._progress_page.reset(total) 1022 for application_page in application_pages: 1023 name = application_page.get_main_basename() 1024 self._progress_page.set_project_name(name) 1025 project = application_page.project 1026 # Initialize a dummy project to apply corrections in 1027 # to be able to present those corrections for approval and 1028 # to finally be able to apply only approved corrections. 1029 dummy = self._copy_project(project) 1030 static_subtitles = dummy.subtitles[:] 1031 for page in assistant_pages: 1032 self._progress_page.set_task_name(page.title) 1033 page.correct_texts(dummy, rows, doc) 1034 self._progress_page.bump_progress() 1035 for i in range(len(static_subtitles)): 1036 orig = project.subtitles[i].get_text(doc) 1037 new = static_subtitles[i].get_text(doc) 1038 if orig == new: continue 1039 changes.append((application_page, i, orig, new)) 1040 self._prepare_confirmation_page(doc, changes) 1041 self.set_current_page(self.get_current_page() + 1) 1042 1043 def _init_properties(self): 1044 """Initialize assistant properties.""" 1045 self.set_title(_("Correct Texts")) 1046 self.add_page(self._introduction_page) 1047 self.add_page(HearingImpairedPage(self)) 1048 if aeidon.util.enchant_and_dicts_available(): 1049 self.add_page(JoinSplitWordsPage(self)) 1050 self.add_page(CommonErrorPage(self)) 1051 self.add_page(CapitalizationPage(self)) 1052 self.add_pages((LineBreakPage(self), LineBreakOptionsPage(self))) 1053 self.add_page(self._progress_page) 1054 self.add_page(self._confirmation_page) 1055 1056 def _init_signal_handlers(self): 1057 """Initialize signal handlers.""" 1058 aeidon.util.connect(self, self, "apply") 1059 aeidon.util.connect(self, self, "cancel") 1060 aeidon.util.connect(self, self, "close") 1061 aeidon.util.connect(self, self, "prepare") 1062 aeidon.util.connect(self, self, "window-state-event") 1063 1064 def _on_apply(self, *args): 1065 """Apply accepted changes to projects.""" 1066 gaupol.util.set_cursor_busy(self) 1067 edits = removals = 0 1068 changes = self._confirmation_page.get_confirmed_changes() 1069 changed_pages = aeidon.util.get_unique([x[0] for x in changes]) 1070 field = self._introduction_page.get_field() 1071 doc = gaupol.util.text_field_to_document(field) 1072 description = _("Correcting texts") 1073 register = aeidon.registers.DO 1074 for page in changed_pages: 1075 indices = [x[1] for x in changes if x[0] is page] 1076 texts = [x[3] for x in changes if x[0] is page] 1077 if indices and texts: 1078 page.project.replace_texts(indices, doc, texts) 1079 page.project.set_action_description(register, description) 1080 edits += len(indices) 1081 indices = [x for i, x in enumerate(indices) if not texts[i]] 1082 if indices and gaupol.conf.text_assistant.remove_blank: 1083 page.project.remove_subtitles(indices) 1084 page.project.group_actions(register, 2, description) 1085 removals += len(indices) 1086 page.view.columns_autosize() 1087 edits = edits - removals 1088 message = _("Edited {edits:d} and removed {removals:d} subtitles") 1089 self.application.flash_message(message.format(**locals())) 1090 gaupol.util.set_cursor_normal(self) 1091 1092 def _on_cancel(self, *args): 1093 """Destroy assistant.""" 1094 self._save_window_geometry() 1095 self.destroy() 1096 1097 def _on_close(self, *args): 1098 """Destroy assistant.""" 1099 self._save_window_geometry() 1100 self.destroy() 1101 1102 def _on_prepare(self, assistant, page): 1103 """Prepare `page` to be shown next.""" 1104 previous_page = self._previous_page 1105 self._previous_page = page 1106 if page is self._introduction_page: 1107 return self._prepare_introduction_page() 1108 pages = self._introduction_page.get_selected_pages() 1109 if page is self._progress_page: 1110 if previous_page is not self._confirmation_page: 1111 return self._prepare_progress_page(pages) 1112 1113 def _on_window_state_event(self, window, event): 1114 """Save window maximization.""" 1115 state = event.new_window_state 1116 maximized = bool(state & Gdk.WindowState.MAXIMIZED) 1117 gaupol.conf.text_assistant.maximized = maximized 1118 1119 def _prepare_confirmation_page(self, doc, changes): 1120 """Present `changes` and activate confirmation page.""" 1121 count = len(changes) 1122 title = n_("Confirm {:d} Change", 1123 "Confirm {:d} Changes", 1124 count).format(count) 1125 1126 self.set_page_title(self._confirmation_page, title) 1127 self._confirmation_page.application = self.application 1128 self._confirmation_page.doc = doc 1129 self._confirmation_page.populate_tree_view(changes) 1130 self.set_page_complete(self._progress_page, True) 1131 1132 def _prepare_introduction_page(self): 1133 """Prepare introduction page content.""" 1134 n = self.get_n_pages() 1135 pages = list(map(self.get_nth_page, range(n))) 1136 pages.remove(self._introduction_page) 1137 pages.remove(self._progress_page) 1138 pages.remove(self._confirmation_page) 1139 pages = [x for x in pages if hasattr(x, "correct_texts")] 1140 self._introduction_page.populate_tree_view(pages) 1141 1142 def _prepare_progress_page(self, pages): 1143 """Prepare progress page for `pages`.""" 1144 self._progress_page.reset(0, True) 1145 self.set_page_complete(self._progress_page, False) 1146 gaupol.util.delay_add(10, self._correct_texts, pages) 1147 1148 def _save_window_geometry(self): 1149 """Save the geometry of the assistant window.""" 1150 if not gaupol.conf.text_assistant.maximized: 1151 gaupol.conf.text_assistant.size = list(self.get_size()) 1152