1__license__ = 'GPL v3' 2__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' 3 4from qt.core import ( 5 Qt, QApplication, QDialog, QIcon, QListWidgetItem) 6 7from calibre.gui2.dialogs.tag_categories_ui import Ui_TagCategories 8from calibre.gui2.dialogs.confirm_delete import confirm 9from calibre.gui2 import error_dialog, warning_dialog 10from calibre.constants import islinux 11from calibre.utils.icu import sort_key, strcmp, primary_contains 12from polyglot.builtins import iteritems 13 14 15class Item: 16 17 def __init__(self, name, label, index, icon, exists): 18 self.name = name 19 self.label = label 20 self.index = index 21 self.icon = icon 22 self.exists = exists 23 24 def __str__(self): 25 return 'name=%s, label=%s, index=%s, exists=%s'%(self.name, self.label, self.index, self.exists) 26 27 28class TagCategories(QDialog, Ui_TagCategories): 29 30 ''' 31 The structure of user_categories stored in preferences is 32 {cat_name: [ [name, category, v], [], []}, cat_name [ [name, cat, v] ...} 33 where name is the item name, category is where it came from (series, etc), 34 and v is a scratch area that this editor uses to keep track of categories. 35 36 If you add a category, it is permissible to set v to zero. If you delete 37 a category, ensure that both the name and the category match. 38 ''' 39 category_labels_orig = ['', 'authors', 'series', 'publisher', 'tags', 'languages'] 40 41 def __init__(self, window, db, on_category=None, book_ids=None): 42 QDialog.__init__(self, window) 43 Ui_TagCategories.__init__(self) 44 self.setupUi(self) 45 self.blank.setText('\xa0') 46 47 # I can't figure out how to get these into the .ui file 48 self.gridLayout_2.setColumnMinimumWidth(0, 50) 49 self.gridLayout_2.setColumnStretch(0, 1) 50 self.gridLayout_2.setColumnMinimumWidth(2, 50) 51 self.gridLayout_2.setColumnStretch(2, 1) 52 53 # Remove help icon on title bar 54 icon = self.windowIcon() 55 self.setWindowFlags(self.windowFlags()&(~Qt.WindowType.WindowContextHelpButtonHint)) 56 self.setWindowIcon(icon) 57 58 self.db = db 59 self.applied_items = [] 60 self.book_ids = book_ids 61 62 if self.book_ids is None: 63 self.apply_vl_checkbox.setEnabled(False) 64 65 cc_icon = QIcon(I('column.png')) 66 67 self.category_labels = self.category_labels_orig[:] 68 self.category_icons = [None, QIcon(I('user_profile.png')), QIcon(I('series.png')), 69 QIcon(I('publisher.png')), QIcon(I('tags.png')), 70 QIcon(I('languages.png'))] 71 self.category_values = [None, 72 lambda: [t.original_name.replace('|', ',') for t in self.db_categories['authors']], 73 lambda: [t.original_name for t in self.db_categories['series']], 74 lambda: [t.original_name for t in self.db_categories['publisher']], 75 lambda: [t.original_name for t in self.db_categories['tags']], 76 lambda: [t.original_name for t in self.db_categories['languages']] 77 ] 78 category_names = ['', _('Authors'), ngettext('Series', 'Series', 2), 79 _('Publishers'), _('Tags'), _('Languages')] 80 81 for key,cc in iteritems(self.db.custom_field_metadata()): 82 if cc['datatype'] in ['text', 'series', 'enumeration']: 83 self.category_labels.append(key) 84 self.category_icons.append(cc_icon) 85 self.category_values.append(lambda col=key: [t.original_name for t in self.db_categories[col]]) 86 category_names.append(cc['name']) 87 elif cc['datatype'] == 'composite' and \ 88 cc['display'].get('make_category', False): 89 self.category_labels.append(key) 90 self.category_icons.append(cc_icon) 91 category_names.append(cc['name']) 92 self.category_values.append(lambda col=key: [t.original_name for t in self.db_categories[col]]) 93 self.categories = dict.copy(db.new_api.pref('user_categories', {})) 94 if self.categories is None: 95 self.categories = {} 96 self.initialize_category_lists(book_ids=None) 97 98 self.display_filtered_categories(0) 99 100 for v in category_names: 101 self.category_filter_box.addItem(v) 102 self.current_cat_name = None 103 104 self.copy_category_name_to_clipboard.clicked.connect(self.copy_category_name_to_clipboard_clicked) 105 self.apply_button.clicked.connect(self.apply_button_clicked) 106 self.unapply_button.clicked.connect(self.unapply_button_clicked) 107 self.add_category_button.clicked.connect(self.add_category) 108 self.rename_category_button.clicked.connect(self.rename_category) 109 self.category_box.currentIndexChanged[int].connect(self.select_category) 110 self.category_filter_box.currentIndexChanged[int].connect( 111 self.display_filtered_categories) 112 self.item_filter_box.textEdited.connect(self.display_filtered_items) 113 self.delete_category_button.clicked.connect(self.del_category) 114 if islinux: 115 self.available_items_box.itemDoubleClicked.connect(self.apply_tags) 116 else: 117 self.available_items_box.itemActivated.connect(self.apply_tags) 118 self.applied_items_box.itemActivated.connect(self.unapply_tags) 119 self.apply_vl_checkbox.clicked.connect(self.apply_vl) 120 121 self.populate_category_list() 122 if on_category is not None: 123 l = self.category_box.findText(on_category) 124 if l >= 0: 125 self.category_box.setCurrentIndex(l) 126 if self.current_cat_name is None: 127 self.category_box.setCurrentIndex(0) 128 self.select_category(0) 129 130 def copy_category_name_to_clipboard_clicked(self): 131 t = self.category_box.itemText(self.category_box.currentIndex()) 132 QApplication.clipboard().setText(t) 133 134 def initialize_category_lists(self, book_ids): 135 self.db_categories = self.db.new_api.get_categories(book_ids=book_ids) 136 self.all_items = [] 137 self.all_items_dict = {} 138 for idx,label in enumerate(self.category_labels): 139 if idx == 0: 140 continue 141 for n in self.category_values[idx](): 142 t = Item(name=n, label=label, index=len(self.all_items), 143 icon=self.category_icons[idx], exists=True) 144 self.all_items.append(t) 145 self.all_items_dict[icu_lower(label+':'+n)] = t 146 147 for cat in self.categories: 148 for item,l in enumerate(self.categories[cat]): 149 key = icu_lower(':'.join([l[1], l[0]])) 150 t = self.all_items_dict.get(key, None) 151 if l[1] in self.category_labels: 152 if t is None: 153 t = Item(name=l[0], label=l[1], index=len(self.all_items), 154 icon=self.category_icons[self.category_labels.index(l[1])], 155 exists=False) 156 self.all_items.append(t) 157 self.all_items_dict[key] = t 158 l[2] = t.index 159 else: 160 # remove any references to a category that no longer exists 161 del self.categories[cat][item] 162 163 self.all_items_sorted = sorted(self.all_items, key=lambda x: sort_key(x.name)) 164 165 def apply_vl(self, checked): 166 if checked: 167 self.initialize_category_lists(self.book_ids) 168 else: 169 self.initialize_category_lists(None) 170 self.fill_applied_items() 171 172 def make_list_widget(self, item): 173 n = item.name if item.exists else item.name + _(' (not on any book)') 174 w = QListWidgetItem(item.icon, n) 175 w.setData(Qt.ItemDataRole.UserRole, item.index) 176 w.setToolTip(_('Category lookup name: ') + item.label) 177 return w 178 179 def display_filtered_items(self, text): 180 self.display_filtered_categories(None) 181 182 def display_filtered_categories(self, idx): 183 idx = idx if idx is not None else self.category_filter_box.currentIndex() 184 self.available_items_box.clear() 185 self.applied_items_box.clear() 186 item_filter = self.item_filter_box.text() 187 for item in self.all_items_sorted: 188 if idx == 0 or item.label == self.category_labels[idx]: 189 if item.index not in self.applied_items and item.exists: 190 if primary_contains(item_filter, item.name): 191 self.available_items_box.addItem(self.make_list_widget(item)) 192 for index in self.applied_items: 193 self.applied_items_box.addItem(self.make_list_widget(self.all_items[index])) 194 195 def apply_button_clicked(self): 196 self.apply_tags(node=None) 197 198 def apply_tags(self, node=None): 199 if self.current_cat_name is None: 200 return 201 nodes = self.available_items_box.selectedItems() if node is None else [node] 202 if len(nodes) == 0: 203 warning_dialog(self, _('No items selected'), 204 _('You must select items to apply'), 205 show=True, show_copy_button=False) 206 return 207 for node in nodes: 208 index = self.all_items[node.data(Qt.ItemDataRole.UserRole)].index 209 if index not in self.applied_items: 210 self.applied_items.append(index) 211 self.applied_items.sort(key=lambda x:sort_key(self.all_items[x].name)) 212 self.display_filtered_categories(None) 213 214 def unapply_button_clicked(self): 215 self.unapply_tags(node=None) 216 217 def unapply_tags(self, node=None): 218 nodes = self.applied_items_box.selectedItems() if node is None else [node] 219 if len(nodes) == 0: 220 warning_dialog(self, _('No items selected'), 221 _('You must select items to unapply'), 222 show=True, show_copy_button=False) 223 return 224 for node in nodes: 225 index = self.all_items[node.data(Qt.ItemDataRole.UserRole)].index 226 self.applied_items.remove(index) 227 self.display_filtered_categories(None) 228 229 def add_category(self): 230 self.save_category() 231 cat_name = str(self.input_box.text()).strip() 232 if cat_name == '': 233 return False 234 comps = [c.strip() for c in cat_name.split('.') if c.strip()] 235 if len(comps) == 0 or '.'.join(comps) != cat_name: 236 error_dialog(self, _('Invalid name'), 237 _('That name contains leading or trailing periods, ' 238 'multiple periods in a row or spaces before ' 239 'or after periods.')).exec() 240 return False 241 for c in sorted(self.categories.keys(), key=sort_key): 242 if strcmp(c, cat_name) == 0 or \ 243 (icu_lower(cat_name).startswith(icu_lower(c) + '.') and 244 not cat_name.startswith(c + '.')): 245 error_dialog(self, _('Name already used'), 246 _('That name is already used, perhaps with different case.')).exec() 247 return False 248 if cat_name not in self.categories: 249 self.category_box.clear() 250 self.current_cat_name = cat_name 251 self.categories[cat_name] = [] 252 self.applied_items = [] 253 self.populate_category_list() 254 self.input_box.clear() 255 self.category_box.setCurrentIndex(self.category_box.findText(cat_name)) 256 return True 257 258 def rename_category(self): 259 self.save_category() 260 cat_name = str(self.input_box.text()).strip() 261 if cat_name == '': 262 return False 263 if not self.current_cat_name: 264 return False 265 comps = [c.strip() for c in cat_name.split('.') if c.strip()] 266 if len(comps) == 0 or '.'.join(comps) != cat_name: 267 error_dialog(self, _('Invalid name'), 268 _('That name contains leading or trailing periods, ' 269 'multiple periods in a row or spaces before ' 270 'or after periods.')).exec() 271 return False 272 273 for c in self.categories: 274 if strcmp(c, cat_name) == 0: 275 error_dialog(self, _('Name already used'), 276 _('That name is already used, perhaps with different case.')).exec() 277 return False 278 # The order below is important because of signals 279 self.categories[cat_name] = self.categories[self.current_cat_name] 280 del self.categories[self.current_cat_name] 281 self.current_cat_name = None 282 self.populate_category_list() 283 self.input_box.clear() 284 self.category_box.setCurrentIndex(self.category_box.findText(cat_name)) 285 return True 286 287 def del_category(self): 288 if self.current_cat_name is not None: 289 if not confirm('<p>'+_('The current User category will be ' 290 '<b>permanently deleted</b>. Are you sure?') + 291 '</p>', 'tag_category_delete', self): 292 return 293 del self.categories[self.current_cat_name] 294 self.current_cat_name = None 295 self.category_box.removeItem(self.category_box.currentIndex()) 296 297 def select_category(self, idx): 298 self.save_category() 299 s = self.category_box.itemText(idx) 300 if s: 301 self.current_cat_name = str(s) 302 else: 303 self.current_cat_name = None 304 self.fill_applied_items() 305 306 def fill_applied_items(self): 307 if self.current_cat_name: 308 self.applied_items = [cat[2] for cat in self.categories.get(self.current_cat_name, [])] 309 else: 310 self.applied_items = [] 311 self.applied_items.sort(key=lambda x:sort_key(self.all_items[x].name)) 312 self.display_filtered_categories(None) 313 314 def accept(self): 315 self.save_category() 316 for cat in sorted(self.categories.keys(), key=sort_key): 317 components = cat.split('.') 318 for i in range(0,len(components)): 319 c = '.'.join(components[0:i+1]) 320 if c not in self.categories: 321 self.categories[c] = [] 322 QDialog.accept(self) 323 324 def save_category(self): 325 if self.current_cat_name is not None: 326 l = [] 327 for index in self.applied_items: 328 item = self.all_items[index] 329 l.append([item.name, item.label, item.index]) 330 self.categories[self.current_cat_name] = l 331 332 def populate_category_list(self): 333 self.category_box.blockSignals(True) 334 self.category_box.clear() 335 self.category_box.addItems(sorted(self.categories.keys(), key=sort_key)) 336 self.category_box.blockSignals(False) 337