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