1# -*- coding: utf-8 -*-
2
3# Copyright (C) 2005 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"""Building and updating dynamic menus."""
19
20import aeidon
21import gaupol
22import os
23import sys
24
25from gi.repository import Gtk
26
27
28class MenuAgent(aeidon.Delegate):
29
30    """Building and updating dynamic menus."""
31
32    def __init__(self, master):
33        """Initialize a :class:`MenuAgent` instance."""
34        aeidon.Delegate.__init__(self, master)
35        self._columns_popup = None
36        self._redo_menu_items = []
37        self._tab_popup = None
38        self._undo_menu_items = []
39        self._view_popup = None
40        self._init_signal_handlers()
41
42    @aeidon.deco.once
43    def _get_recent_chooser_menu(self):
44        """Return a new recent chooser menu."""
45        menu = Gtk.RecentChooserMenu()
46        menu.set_show_icons(sys.platform != "win32")
47        menu.set_show_not_found(False)
48        menu.set_show_numbers(False)
49        menu.set_show_tips(True)
50        menu.set_sort_type(Gtk.RecentSortType.MRU)
51        menu.connect("item-activated", self._on_recent_menu_item_activated)
52        recent_filter = Gtk.RecentFilter()
53        recent_filter.add_application("gaupol")
54        menu.add_filter(recent_filter)
55        menu.set_filter(recent_filter)
56        menu.set_limit(10)
57        return menu
58
59    def _init_signal_handlers(self):
60        """Initialize signal handlers."""
61        self.connect("page-added", self._update_projects_menu)
62        self.connect("page-closed", self._update_projects_menu)
63        self.connect("page-saved", self._update_projects_menu)
64        self.connect("page-switched", self._update_projects_menu)
65        self.connect("pages-reordered", self._update_projects_menu)
66        self.connect("init-done", self._update_recent_menus)
67        self.connect("page-added", self._update_recent_menus)
68        self.connect("page-saved", self._update_recent_menus)
69
70    @aeidon.deco.export
71    def _on_activate_project_activate(self, action, parameter):
72        """Activate the requested page."""
73        index = int(parameter.get_string())
74        self.set_current_page(self.pages[index])
75
76    @aeidon.deco.export
77    def _on_open_button_show_menu(self, *args):
78        """Show a menu listing recent files to open."""
79        menu = self._get_recent_chooser_menu()
80        self.open_button.set_menu(menu)
81
82    @aeidon.deco.export
83    def _on_open_recent_main_file_activate(self, action, *args):
84        """Open recent file as main document."""
85        self.open_main(action.gaupol_path)
86
87    def _on_recent_menu_item_activated(self, chooser, *args):
88        """Open recent file as main document."""
89        uri = chooser.get_current_uri()
90        path = aeidon.util.uri_to_path(uri)
91        self.open_button.get_menu().deactivate()
92        self.open_main(path)
93
94    @aeidon.deco.export
95    def _on_open_recent_translation_file_activate(self, action, *args):
96        """Open recent file as translation document."""
97        self.open_translation(action.gaupol_path)
98
99    @aeidon.deco.export
100    def _on_redo_button_show_menu(self, *args):
101        """Show a menu listing all redoable actions."""
102        if not self.redo_button.get_menu():
103            self.redo_button.set_menu(Gtk.Menu())
104        menu = self.redo_button.get_menu()
105        for item in menu.get_children():
106            menu.remove(item)
107        self._redo_menu_items = []
108        redoables = []
109        with aeidon.util.silent(AttributeError):
110            # XXX: Gtk.Actionable.set_action_name doesn't affect the dropdown
111            # arrow making it clickable even when there's nothing to redo.
112            page = self.get_current_page()
113            redoables = page.project.redoables
114        for i, action in enumerate(redoables):
115            item = Gtk.MenuItem(label=action.description)
116            item.gaupol_index = i
117            item.connect("activate", self._on_redo_menu_item_activate)
118            item.connect("enter-notify-event", self._on_redo_menu_item_enter_notify_event)
119            item.connect("leave-notify-event", self._on_redo_menu_item_leave_notify_event)
120            self._redo_menu_items.append(item)
121            menu.append(item)
122        menu.show_all()
123        self.redo_button.set_menu(menu)
124
125    def _on_redo_menu_item_activate(self, menu_item):
126        """Redo the selected action and all those above it."""
127        self.redo(menu_item.gaupol_index + 1)
128        self.redo_button.get_menu().deactivate()
129
130    def _on_redo_menu_item_enter_notify_event(self, menu_item, event):
131        """Select all actions above `menu_item`."""
132        index = menu_item.gaupol_index
133        for item in self._redo_menu_items[:index]:
134            item.set_state(Gtk.StateType.PRELIGHT)
135
136    def _on_redo_menu_item_leave_notify_event(self, menu_item, event):
137        """Unselect all actions above `menu_item`."""
138        index = menu_item.gaupol_index
139        for item in self._redo_menu_items[:index]:
140            item.set_state(Gtk.StateType.NORMAL)
141
142    @aeidon.deco.export
143    def _on_tab_widget_button_press_event(self, button, event, page):
144        """Display a pop-up menu with tab-related actions."""
145        if event.button != 3: return
146        if self._tab_popup is None:
147            path = os.path.join(aeidon.DATA_DIR, "ui", "tab-popup.ui")
148            builder = Gtk.Builder.new_from_file(path)
149            self._tab_popup = builder.get_object("tab-popup")
150        menu = Gtk.Menu.new_from_model(self._tab_popup)
151        menu.attach_to_widget(self.notebook, None)
152        menu.popup(parent_menu_shell=None,
153                   parent_menu_item=None,
154                   func=None,
155                   data=None,
156                   button=event.button,
157                   activate_time=event.time)
158
159        return True
160
161    @aeidon.deco.export
162    def _on_undo_button_show_menu(self, *args):
163        """Show a menu listing all undoable actions."""
164        if not self.undo_button.get_menu():
165            self.undo_button.set_menu(Gtk.Menu())
166        menu = self.undo_button.get_menu()
167        for item in menu.get_children():
168            menu.remove(item)
169        self._undo_menu_items = []
170        undoables = []
171        with aeidon.util.silent(AttributeError):
172            # XXX: Gtk.Actionable.set_action_name doesn't affect the dropdown
173            # arrow making it clickable even when there's nothing to undo.
174            page = self.get_current_page()
175            undoables = page.project.undoables
176        for i, action in enumerate(undoables):
177            item = Gtk.MenuItem(label=action.description)
178            item.gaupol_index = i
179            item.connect("activate", self._on_undo_menu_item_activate)
180            item.connect("enter-notify-event", self._on_undo_menu_item_enter_notify_event)
181            item.connect("leave-notify-event", self._on_undo_menu_item_leave_notify_event)
182            self._undo_menu_items.append(item)
183            menu.append(item)
184        menu.show_all()
185        self.undo_button.set_menu(menu)
186
187    def _on_undo_menu_item_activate(self, menu_item):
188        """Undo the selected action and all those above it."""
189        self.undo(menu_item.gaupol_index + 1)
190        self.undo_button.get_menu().deactivate()
191
192    def _on_undo_menu_item_enter_notify_event(self, menu_item, event):
193        """Select all actions above `menu_item`."""
194        index = menu_item.gaupol_index
195        for item in self._undo_menu_items[:index]:
196            item.set_state(Gtk.StateType.PRELIGHT)
197
198    def _on_undo_menu_item_leave_notify_event(self, menu_item, event):
199        """Unselect all actions above `menu_item`."""
200        index = menu_item.gaupol_index
201        for item in self._undo_menu_items[:index]:
202            item.set_state(Gtk.StateType.NORMAL)
203
204    @aeidon.deco.export
205    def _on_view_button_press_event(self, view, event):
206        """Display a right-click pop-up menu to edit data."""
207        if event.button != 3: return
208        x = int(event.x)
209        y = int(event.y)
210        value = view.get_path_at_pos(x, y)
211        if value is None: return
212        path, column, x, y = value
213        row = gaupol.util.tree_path_to_row(path)
214        if not row in view.get_selected_rows():
215            view.set_cursor(path, column)
216            view.update_headers()
217        if self._view_popup is None:
218            path = os.path.join(aeidon.DATA_DIR, "ui", "view-popup.ui")
219            builder = Gtk.Builder.new_from_file(path)
220            self._view_popup = builder.get_object("view-popup")
221        menu = Gtk.Menu.new_from_model(self._view_popup)
222        menu.attach_to_widget(view, None)
223        menu.popup(parent_menu_shell=None,
224                   parent_menu_item=None,
225                   func=None,
226                   data=None,
227                   button=event.button,
228                   activate_time=event.time)
229
230        return True
231
232    @aeidon.deco.export
233    def _on_view_header_button_press_event(self, button, event):
234        """Display a column visibility pop-up menu."""
235        if event.button != 3: return
236        if self._columns_popup is None:
237            path = os.path.join(aeidon.DATA_DIR, "ui", "columns-popup.ui")
238            builder = Gtk.Builder.new_from_file(path)
239            self._columns_popup = builder.get_object("columns-popup")
240        menu = Gtk.Menu.new_from_model(self._columns_popup)
241        menu.attach_to_widget(self.get_current_page().view, None)
242        menu.popup(parent_menu_shell=None,
243                   parent_menu_item=None,
244                   func=None,
245                   data=None,
246                   button=event.button,
247                   activate_time=event.time)
248
249        return True
250
251    def _update_projects_menu(self, *args):
252        """Update the project menu list of projects."""
253        menu = self.get_menubar_section("projects-placeholder")
254        # Menubar not available when running unit tests.
255        if menu is None: return
256        menu.remove_all()
257        current = self.get_current_page()
258        for i, page in enumerate(self.pages):
259            label = page.get_main_basename()
260            if len(label) > 100:
261                label = label[:100] + "…"
262            action = "win.activate-project::{:d}".format(i)
263            menu.append(label, action)
264            if page is current:
265                action = self.get_action("activate-project")
266                action.set_state(str(i))
267
268    def _update_recent_main_menu(self, *args):
269        """Update the file menu list of recent main files."""
270        menu = self.get_menubar_section("open-recent-main-placeholder")
271        # Menubar not available when running unit tests.
272        if menu is None: return
273        menu.remove_all()
274        recent = self._get_recent_chooser_menu()
275        for i, uri in enumerate(recent.get_uris()):
276            path = aeidon.util.uri_to_path(uri)
277            label = os.path.basename(path)
278            if len(label) > 100:
279                label = label[:100] + "…"
280            action = "win.open-recent-main-file-{:d}".format(i)
281            menu.append(label, action)
282            action = action.replace("win.", "")
283            if self.get_action(action):
284                # If action i exists, update the file path.
285                self.get_action(action).gaupol_path = path
286            else:
287                # Otherwise, create the action and add to self.window.
288                # XXX: For some reason, this can sometimes fail.
289                with aeidon.util.silent(Exception):
290                    ao = gaupol.Action(action)
291                    ao.gaupol_path = path
292                    callback = self._on_open_recent_main_file_activate
293                    ao.connect("activate", callback)
294                    self.window.add_action(ao)
295
296    def _update_recent_menus(self, *args):
297        """Update the file menu lists of recent files."""
298        self._update_recent_main_menu()
299        self._update_recent_translation_menu()
300        # Update enabled states of added actions.
301        self.update_gui()
302
303    def _update_recent_translation_menu(self, *args):
304        """Update the file menu list of recent translation files."""
305        menu = self.get_menubar_section("open-recent-translation-placeholder")
306        # Menubar not available when running unit tests.
307        if menu is None: return
308        menu.remove_all()
309        recent = self._get_recent_chooser_menu()
310        for i, uri in enumerate(recent.get_uris()):
311            path = aeidon.util.uri_to_path(uri)
312            label = os.path.basename(path)
313            if len(label) > 100:
314                label = label[:100] + "…"
315            action = "win.open-recent-translation-file-{:d}".format(i)
316            menu.append(label, action)
317            action = action.replace("win.", "")
318            if not self.get_action(action):
319                ao = gaupol.OpenRecentTranslationFileAction(action)
320                callback = self._on_open_recent_translation_file_activate
321                ao.connect("activate", callback)
322                self.window.add_action(ao)
323            self.get_action(action).gaupol_path = path
324