gaupol/page.py

Source code for module gaupol.page from file gaupol/page.py.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
# -*- coding: utf-8 -*-

# Copyright (C) 2005 Osmo Salomaa
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""User interface container and controller for :class:`aeidon.Project`."""

import aeidon
import gaupol
import os
import sys
_ = aeidon.i18n._

from gi.repository import Gtk
from gi.repository import Pango

__all__ = ("Page",)


class Page(aeidon.Observable):

    """
    User interface container and controller for :class:`aeidon.Project`.

    :ivar edit_mode: :attr:`aeidon.modes` item corresponding to editing mode
    :ivar project: The associated :class:`aeidon.Project` instance
    :ivar tab_label: :class:`Gtk.Label` contained in :attr:`tab_widget`
    :ivar tab_widget: Widget that can be placed in a notebook tab
    :ivar untitle: Title used if :attr:`project.main_file` is unsaved
    :ivar view: The associated :class:`gaupol.View` instance

    Signals and their arguments for callback functions:
     * ``close-request``: page
     * ``view-created``: page, view

    This class represents one page in a notebook of user interfaces for
    projects. The view is updated automatically when project data changes.
    """
    signals = ("close-request", "view-created")

    def __init__(self, count=0):
        """Initialize a :class:`Page` instance."""
        aeidon.Observable.__init__(self)
        self.edit_mode = gaupol.conf.editor.mode
        self.project = None
        self.tab_label = None
        self.tab_widget = None
        self.untitle = _("Untitled {:d}").format(count)
        self.view = gaupol.View(self.edit_mode)
        self._init_project()
        self._init_widgets()
        self._init_signal_handlers()
        self.update_tab_label()
        self.emit("view-created", self.view)

    def document_to_text_column(self, doc):
        """Translate document enumeration to view's column enumeration."""
        if doc == aeidon.documents.MAIN:
            return self.view.columns.MAIN_TEXT
        if doc == aeidon.documents.TRAN:
            return self.view.columns.TRAN_TEXT
        raise ValueError("Invalid document: {}"
                         .format(repr(doc)))

    def get_main_basename(self):
        """Return basename of the main document."""
        if self.project.main_file is not None:
            return os.path.basename(self.project.main_file.path)
        return self.untitle

    def _get_subtitle_value(self, row, field):
        """Return value of subtitle data for `row` and `field`."""
        mode = self.edit_mode
        subtitle = self.project.subtitles[row]
        if field == gaupol.fields.START:
            return subtitle.get_start(mode)
        if field == gaupol.fields.END:
            return subtitle.get_end(mode)
        if field == gaupol.fields.DURATION:
            if mode == aeidon.modes.TIME:
                return subtitle.duration_seconds
            if mode == aeidon.modes.FRAME:
                return subtitle.duration_frame
            raise ValueError("Invalid mode: {}"
                             .format(repr(mode)))

        if field == gaupol.fields.MAIN_TEXT:
            return subtitle.main_text
        if field == gaupol.fields.TRAN_TEXT:
            return subtitle.tran_text
        raise ValueError("Invalid field: {}"
                         .format(repr(field)))

    def _get_tab_close_button(self):
        """Initialize and return a tab close button."""
        button = Gtk.Button()
        button.set_name("gaupol-tab-close-button")
        image = gaupol.util.get_icon_image("window-close-symbolic",
                                           "window-close",
                                           Gtk.IconSize.MENU)

        button.add(image)
        button.set_relief(Gtk.ReliefStyle.NONE)
        button.set_focus_on_click(False)
        width = image.get_preferred_width()[1]
        height = image.get_preferred_height()[1]
        padding = (6 if sys.platform == "win32" else 2)
        button.set_size_request(width + padding, height + padding)
        request_close = lambda x, self: self.emit("close-request")
        button.connect("clicked", request_close, self)
        button.set_tooltip_text(_("Close project"))
        return button

    def get_translation_basename(self):
        """Return basename of the translation document."""
        if self.project.tran_file is not None:
            return os.path.basename(self.project.tran_file.path)
        basename = self.get_main_basename()
        if self.project.main_file is not None:
            extension = self.project.main_file.format.extension
            if basename.endswith(extension):
                basename = basename[:-len(extension)]
        return _("{} translation").format(basename)

    def _init_project(self):
        """Initialize :class:`aeidon.Project` with proper properties."""
        framerate = gaupol.conf.editor.framerate
        self.project = aeidon.Project(framerate)

    def _init_signal_handlers(self):
        """Initialize signal handlers."""
        self._init_signal_handlers_for_data()
        self._init_signal_handlers_for_tab_label()

    def _init_signal_handlers_for_data(self):
        """Initialize signal handlers for project data updates."""
        aeidon.util.connect(self, "project", "main-file-opened")
        aeidon.util.connect(self, "project", "main-texts-changed")
        aeidon.util.connect(self, "project", "positions-changed")
        aeidon.util.connect(self, "project", "subtitles-changed")
        aeidon.util.connect(self, "project", "subtitles-inserted")
        aeidon.util.connect(self, "project", "subtitles-removed")
        aeidon.util.connect(self, "project", "translation-file-opened")
        aeidon.util.connect(self, "project", "translation-texts-changed")

    def _init_signal_handlers_for_tab_label(self):
        """Initialize signal handlers for tab label updates."""
        aeidon.util.connect(self, "tab_label", "query-tooltip")
        update_label = lambda *args: args[-1].update_tab_label()
        self.project.connect("main-file-opened", update_label, self)
        self.project.connect("main-file-saved", update_label, self)
        self.project.connect("notify::main_changed", update_label, self)
        self.project.connect("notify::tran_changed", update_label, self)

    def _init_widgets(self):
        """Initialize widgets to use in a notebook tab."""
        self.tab_label = Gtk.Label()
        self.tab_label.set_halign(Gtk.Align.CENTER)
        self.tab_label.set_ellipsize(Pango.EllipsizeMode.MIDDLE)
        # Set minimum width for tab label. The actual width taken
        # depends on window size, amount of tabs and notebook
        # child properties 'tab-expand' and 'tab-fill'.
        width = gaupol.util.char_to_px(24)
        self.tab_label.set_size_request(width, -1)
        self.tab_label.set_tooltip_text(self.untitle)
        button = self._get_tab_close_button()
        box = gaupol.util.new_hbox(spacing=4)
        gaupol.util.pack_start_expand(box, self.tab_label)
        gaupol.util.pack_start(box, button)
        box.gaupol_button = button
        self.tab_widget = Gtk.EventBox()
        self.tab_widget.add(box)
        self.tab_widget.set_visible_window(False)
        self.tab_widget.show_all()

    def _on_project_main_file_opened(self, *args):
        """Reload the entire view."""
        self.reload_view_all()
        gaupol.util.iterate_main()

    def _on_project_main_texts_changed(self, project, rows):
        """Reload and select main texts in rows."""
        if not rows: return
        fields = (gaupol.fields.MAIN_TEXT,)
        self.reload_view(rows, fields)
        if self.view.get_focus()[0] not in rows:
            col = self.view.columns.MAIN_TEXT
            self.view.set_focus(rows[0], col)
        self.view.select_rows(rows)
        gaupol.util.iterate_main()

    def _on_project_positions_changed(self, project, rows):
        """Reload and select positions in rows."""
        if not rows: return
        enum = gaupol.fields
        fields = (enum.START, enum.END, enum.DURATION)
        self.reload_view(rows, fields)
        if self.view.get_focus()[0] not in rows:
            self.view.set_focus(rows[0])
        self.view.select_rows(rows)
        gaupol.util.iterate_main()

    def _on_project_subtitles_changed(self, project, rows):
        """Reload and select subtitles in rows."""
        if not rows: return
        fields = [x for x in gaupol.fields]
        fields.remove(gaupol.fields.NUMBER)
        self.reload_view(rows, fields)
        if self.view.get_focus()[0] not in rows:
            self.view.set_focus(rows[0])
        self.view.select_rows(rows)
        gaupol.util.iterate_main()

    def _on_project_subtitles_inserted(self, project, rows):
        """Insert rows to the view and select them."""
        if not rows: return
        mode = self.edit_mode
        store = self.view.get_model()
        for row in sorted(rows):
            subtitle = self.project.subtitles[row]
            store.insert(row)
            store[row][0] = row + 1
            store[row][1] = subtitle.get_start(mode)
            store[row][2] = subtitle.get_end(mode)
            if mode == aeidon.modes.TIME:
                store[row][3] = subtitle.duration_seconds
            if mode == aeidon.modes.FRAME:
                store[row][3] = subtitle.duration_frame
            store[row][4] = subtitle.main_text
            store[row][5] = subtitle.tran_text
        self.view.set_focus(rows[0])
        self.view.select_rows(rows)
        gaupol.util.iterate_main()

    def _on_project_subtitles_removed(self, project, rows):
        """Remove rows from the view."""
        if not rows: return
        store = self.view.get_model()
        if len(rows) > 50:
            # Unset and later reset the model if removing a large amount
            # of rows, because a large batch of separate live updates
            # directly made to the view are slow.
            self.view.set_model(None)
        for row in reversed(sorted(rows)):
            path = gaupol.util.tree_row_to_path(row)
            store.remove(store.get_iter(path))
        if len(rows) > 50:
            self.view.set_model(store)
        if self.project.subtitles:
            row = min(rows[0], len(self.project.subtitles)-1)
            col = self.view.get_focus()[1]
            self.view.set_focus(row, col)
        gaupol.util.iterate_main()

    def _on_project_translation_file_opened(self, *args):
        """Reload the entire view."""
        self.reload_view_all()
        gaupol.util.iterate_main()

    def _on_project_translation_texts_changed(self, project, rows):
        """Reload and select translation texts in rows."""
        if not rows: return
        fields = (gaupol.fields.TRAN_TEXT,)
        self.reload_view(rows, fields)
        if self.view.get_focus()[0] not in rows:
            col = self.view.columns.TRAN_TEXT
            self.view.set_focus(rows[0], col)
        self.view.select_rows(rows)
        gaupol.util.iterate_main()

    def _on_tab_label_query_tooltip(self, label, x, y, keyboard, tooltip):
        """Update the text in the tab tooltip."""
        if self.project.main_file is None: return
        path = self.project.main_file.path
        format = self.project.main_file.format
        encoding = self.project.main_file.encoding
        encoding = aeidon.encodings.code_to_long_name(encoding)
        newline = self.project.main_file.newline
        tooltip.set_markup("{}\n{}\n{}\n{}".format(
                "<b>{}</b> {}".format(_("Path:"), path),
                "<b>{}</b> {}".format(_("Format:"), format.label),
                "<b>{}</b> {}".format(_("Encoding:"), encoding),
                "<b>{}</b> {}".format(_("Newlines:"), newline.label)))

        return True # to show the tooltip.

    def reload_view(self, rows, fields):
        """Reload the view in `rows` and `fields`."""
        store = self.view.get_model()
        for row, field in ((x, y) for x in rows for y in fields):
            value = self._get_subtitle_value(row, field)
            store[row][field] = value

    def reload_view_all(self):
        """Clear and repopulate the entire view."""
        store = self.view.get_model()
        self.view.set_model(None)
        store.clear()
        mode = self.edit_mode
        for i, subtitle in enumerate(self.project.subtitles):
            store.insert(i)
            store[i][0] = i + 1
            store[i][1] = subtitle.get_start(mode)
            store[i][2] = subtitle.get_end(mode)
            if mode == aeidon.modes.TIME:
                store[i][3] = subtitle.duration_seconds
            if mode == aeidon.modes.FRAME:
                store[i][3] = subtitle.duration_frame
            store[i][4] = subtitle.main_text
            store[i][5] = subtitle.tran_text
        self.view.set_model(store)

    def text_column_to_document(self, col):
        """Translate view's column enumeration to document enumeration."""
        if col == self.view.columns.MAIN_TEXT:
            return aeidon.documents.MAIN
        if col == self.view.columns.TRAN_TEXT:
            return aeidon.documents.TRAN
        raise ValueError("Invalid column: {}"
                         .format(repr(col)))

    def update_tab_label(self):
        """Update the notebook tab label and return title."""
        title = self.get_main_basename()
        if self.project.main_changed or self.project.tran_changed:
            title = "*{}".format(title)
        # Adwaita theme uses bold notebook tab labels since 3.12.
        self.tab_label.set_markup("<b>{}</b>".format(title))
        return title