g=d|k^VkIn+O^~{fhe;rxXJs-kzIc2BpYcWD;OR=%
zyy7}zc>1*!B^@w~c6PujZQvnheQ`pUWgd=CDJV+scsTkbgsacLkX=uWDNLyx57ES+
zhdXcZ5y*v4EG^W()8dgs{i^00@@8U(Ju=3=T2Pj^&Os@~oP$p~Bw>f1aXWZ;(r}~8
z{9*&Qw~OOX0O$*PTO0UtZ-N}`Vg2S!0|c+Zu_1Q*bl@!1IY-QCWy(L`*@$GG!ZiiV
z(IB6;FOqF#LjO{II*z&D_r;gjfv|cm2R~3mo&D^2i9Lb5w$E1+5(!JoYzNw;lV+Q<
z`G`})sXeRSa2w-CJx>HIoB2L+xU+q)KSYX{`=!z8O6e(__BHU^Y9%vcq-y;>(f=O7d__
znsQdI&T|haXPQ_dGd8V6PV--s`Ca(Zf+96+kM7EQa8@n18SUI
z^hbUGRs2Zq5)vlM4xc-Zj;Ecx+jqED!5)@IN5wEsQ!X1F8?*U^MJtv;6J?p;Nh2kg
zcel6*4RYxkvCgoJ1hsONX&+RaOZjhQXb+tD#%&teFTq}cW@cuQ
z`1CO)=Fgq<7rYbkryVc3P1;ofImu7eYTfL2w*IJ=3GwmXc=Zi{%F5!$ry}ahjQPsvF1Z9_Lvi)3L-SMcIevZwVk5}qo
z_)*gXuxndGE)Mx;kYJ5+OFHA}7j{tbnh^9d4D^@z6)F-P^A{OVDas3se{X_Hv!9&_
z$^?)DtA&8Dd}Ov`
z@yQPwB@{bF^uLv*X(9H!N&a=6?DX|n$7Im7T4EvbU>m&5mn?Wo3gov3R_A#gb0+rb
zv;R!rxIi~CjONHiGBpgFJ6I2YK7WjhFLuskz?!FhoTThZTR<>sLze91Zg3{86!$?#BjX`D@sWP(W^OMF!KfC$!5o=DSM-^HzPbC&R>Bp3
z*M=!Q>BeMh3+*L?@4?t*8%h5)%xnf35;p%bwXH+8+ehAu!1~4NmVG3qajH5jx
zH|OKU(;|}g#M6%`7sw?jLa!evxzL5WI;oLWvWeeWL~dZgZH5vW5rsU$`zgnvNcv2X
z=1$0wDXQ${Y+q$f<0Gv3JNRr>y*KSZqIpEqV=<4f+SLiFKlAD~QV{NgA1aX%7f6cZ
zSLM749BwbzC%Nm*pVy_+M)OtRn+*9u+HDG3zfjK(mj@Mb#*tanaixsw^civ1S2=R|
zq>oSyK;Kb@p!K}fTtBesylKvAbr6-q*z1D~v?Z^2-nl8A-f5x`Deq0DmgLKZpP67cHXPBWtWq~W?2kB-5f%ReV4Tqw@>p#Z@!fwBZ|~d8XN=
z#jKC4%!3RnrkEZxjt~%7>?xxAxgr35+%9ns?1tjBOmn<73CMB!@)m8yOi|6j^IArb
zgqX}g11~AOr8EEV6!|cvSC=%JQ>nJogUxH^mexmWF7))edan^2__#y0^sNGYQbYdx
zi_pLG(nvUXtTvN;}-s`yd^^cwEV7H3Rp+jhl=?>I|}Yc|G58
z>g7+b{dTZntiidttc$o)X>Eb_SFOn4G7T)SS>g#51rE@Bk>8T^*GY@0PVr;x`
z{)AxMJ9MAp(R=#Uty^2nBo@oi(VC`N#;)@x
zRdDA0b@!UHiQlC<{XR?iH)yc^Nn`(`o2zsP*yZ%^hSeo$(KME@JDP{*R}cuLRY~rZ
zjxIm*Yx^Yj;C+G#Le&v_xhuV=ri)>9r(_dY$n|}ToBRl-`;Qm@y`vw>@^xhuoctv0
z;E6kQ^gc;e*JCSE0}H{m9_oDQ#|?Jx%jIOY%nSouvcC8Iz-6^`8?9-cwWR&!SSS%O
z@k%@QVQBFTQD;Vxm?ugn_GX)U+TqXaAB=|rV}YSP@sSn&Z4%Yra42JjUI#&cP^gs8
znC2R0TE(v5gv6v5Bk`Cfu{3FR>c;>p)Y)fzL=RNOBUj6wa(oM;Um!R!mX`rA^1UXfjWADiS`a)j~
zxUBpxaD*XB4g;p!`+S(Hs5ZOHWG^
zwvbQ~3MRcLrq8(K&baB*<2Ohv))wiyIMRGJbCigzi+o?dDL&4un;Utung47d8_2UJ
zR#e9_b>J{08ic-dWGQ|f*#v{`+d}H~MGxn*CACYt?x&?5*Z2}@FJ=;~5^wX&$AhEj
zp{23N{!HNNOe6blaT3f^>3X?h^!LsrDUzzX{0ZgqE@#uA7yvu?uF|pFZBMq?0SW
z{aA4&k6DSC=z2V64tLjMs1+e%hb$`!A(lQ>^%Dl#tg0o5S{{GzmqYadh6}CS5>#mp0Ly_jZ7u45*ceFOT
zpEYStKeWD}4y`-PqN#j+-~FIP48i<->@zq0Wm_I@N%{8%s*(Alb)utup2KDG1Zso3
zzvw!Y>zk&_Mc!2@L%n>MX`|GSOqMVhhsN3l&A#tOdKROLX!#|~7P9QbH}OJ9=dBmS)u*W2>pY(b%|KB@RMEUlyTV=}
z%SY-ZQ>p)w6h3gn@S~uE;mm)_a!v$Bki)D0(%cEky_W)zuK#)Sn0`WTO4&Hps97Tc
P0WT$ab-5Z@^YDKISm>XQ
literal 0
HcmV?d00001
From 128bd8aa1b9bf7a3106c3ba1a7b6baf5ef095d39 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Wed, 23 Feb 2011 14:05:54 +0000
Subject: [PATCH 17/55] Add right-click rename and create to user categories
---
src/calibre/gui2/tag_view.py | 95 +++++++++++++++++++++++++++++-------
1 file changed, 77 insertions(+), 18 deletions(-)
diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py
index 1033957656..b8ba785d66 100644
--- a/src/calibre/gui2/tag_view.py
+++ b/src/calibre/gui2/tag_view.py
@@ -73,6 +73,7 @@ class TagsView(QTreeView): # {{{
refresh_required = pyqtSignal()
tags_marked = pyqtSignal(object)
user_category_edit = pyqtSignal(object)
+ add_subcategory = pyqtSignal(object)
tag_list_edit = pyqtSignal(object, object)
saved_search_edit = pyqtSignal(object)
author_sort_edit = pyqtSignal(object, object)
@@ -218,6 +219,9 @@ class TagsView(QTreeView): # {{{
if action == 'manage_categories':
self.user_category_edit.emit(category)
return
+ if action == 'add_subcategory':
+ self.add_subcategory.emit(category)
+ return
if action == 'search':
self._toggle(index, set_to=search_state)
return
@@ -303,9 +307,21 @@ class TagsView(QTreeView): # {{{
search_state=TAG_SEARCH_STATES['mark_minus'],
index=index))
self.context_menu.addSeparator()
+ elif key.startswith('@'):
+ if item.can_edit:
+ self.context_menu.addAction(_('Rename %s')%key[1:],
+ partial(self.context_menu_handler, action='edit_item',
+ category=key, index=index))
+ self.context_menu.addAction(self.search_icon,
+ _('Add sub-category to %s')%key[1:],
+ partial(self.context_menu_handler,
+ action='add_subcategory', category=key))
+ self.context_menu.addSeparator()
# Hide/Show/Restore categories
- self.context_menu.addAction(_('Hide category %s') % category,
- partial(self.context_menu_handler, action='hide', category=category))
+ if not key.startswith('@') or key.find('.') < 0:
+ self.context_menu.addAction(_('Hide category %s') % category,
+ partial(self.context_menu_handler, action='hide',
+ category=category))
if self.hidden_categories:
m = self.context_menu.addMenu(_('Show category'))
for col in sorted(self.hidden_categories, key=sort_key):
@@ -615,9 +631,10 @@ class TagsModel(QAbstractItemModel): # {{{
else:
tt = _(u'The lookup/search name is "{0}"').format(r)
- if r.startswith('@') and r.find('.') >= 0:
+ if r.startswith('@'):
path_parts = [p.strip() for p in r.split('.') if p.strip()]
path = ''
+ last_category_node = self.root_item
for i,p in enumerate(path_parts):
path += p
if path not in category_node_map:
@@ -629,6 +646,7 @@ class TagsModel(QAbstractItemModel): # {{{
last_category_node = node
category_node_map[path] = node
self.category_nodes.append(node)
+ node.can_edit = i == (len(path_parts) - 1)
else:
last_category_node = category_node_map[path]
path += '.'
@@ -986,6 +1004,37 @@ class TagsModel(QAbstractItemModel): # {{{
_('An item cannot be set to nothing. Delete it instead.')).exec_()
return False
item = index.internalPointer()
+ if item.type == TagTreeItem.CATEGORY and item.category_key.startswith('@'):
+ user_cats = self.db.prefs.get('user_categories', {})
+ ckey = item.category_key[1:]
+ dotpos = ckey.rfind('.')
+ if dotpos < 0:
+ nkey = val
+ else:
+ nkey = ckey[:dotpos+1] + val
+ for c in user_cats:
+ if c.startswith(ckey):
+ if len(c) == len(ckey):
+ if nkey in user_cats:
+ error_dialog(self.tags_view, _('Rename user category'),
+ _('The name %s is already used'%nkey), show=True)
+ return False
+ user_cats[nkey] = user_cats[ckey]
+ del user_cats[ckey]
+ elif c[len(ckey)] == '.':
+ rest = c[len(ckey):]
+ if (nkey + rest) in user_cats:
+ error_dialog(self.tags_view, _('Rename user category'),
+ _('The name %s is already used')%(nkey+rest), show=True)
+ return False
+ user_cats[nkey + rest] = user_cats[ckey + rest]
+ del user_cats[ckey + rest]
+ self.db.prefs.set('user_categories', user_cats)
+ self.tags_view.set_new_model()
+ # must not use 'self' below because the model has changed!
+ p = self.tags_view.model().find_category_node('@' + nkey)
+ self.tags_view.model().show_item_at_path(p)
+ return True
itm = item.parent
while itm.type != TagTreeItem.CATEGORY:
itm = itm.parent
@@ -1118,15 +1167,6 @@ class TagsModel(QAbstractItemModel): # {{{
for t in tag_item.children:
process_tag(t)
-# def process_level(category_index):
-# for j in xrange(self.rowCount(category_index)):
-# tag_index = self.index(j, 0, category_index)
-# tag_item = tag_index.internalPointer()
-# if tag_item.type == TagTreeItem.CATEGORY:
-# process_level(tag_index)
-# else:
-# process_tag(tag_index, tag_item)
-
for t in self.root_item.children:
process_tag(t)
@@ -1239,7 +1279,7 @@ class TagsModel(QAbstractItemModel): # {{{
break
return self.path_found
- def find_category_node(self, key):
+ def find_category_node(self, key, parent=QModelIndex()):
'''
Search for an category node (a top-level node) in the tags browser list
that matches the key (exact case-insensitive match). Returns the path to
@@ -1248,11 +1288,17 @@ class TagsModel(QAbstractItemModel): # {{{
if not key:
return None
- for i in xrange(self.rowCount(QModelIndex())):
- idx = self.index(i, 0, QModelIndex())
- ckey = idx.internalPointer().category_key
- if strcmp(ckey, key) == 0:
- return self.path_for_index(idx)
+ for i in xrange(self.rowCount(parent)):
+ idx = self.index(i, 0, parent)
+ node = idx.internalPointer()
+ if node.type == TagTreeItem.CATEGORY:
+ ckey = node.category_key
+ if strcmp(ckey, key) == 0:
+ return self.path_for_index(idx)
+ if len(node.children):
+ v = self.find_category_node(key, idx)
+ if v is not None:
+ return v
return None
def show_item_at_path(self, path, box=False):
@@ -1307,6 +1353,7 @@ class TagBrowserMixin(object): # {{{
self.tags_view.tags_marked.connect(self.search.set_search_string)
self.tags_view.tag_list_edit.connect(self.do_tags_list_edit)
self.tags_view.user_category_edit.connect(self.do_user_categories_edit)
+ self.tags_view.add_subcategory.connect(self.do_add_subcategory)
self.tags_view.saved_search_edit.connect(self.do_saved_search_edit)
self.tags_view.author_sort_edit.connect(self.do_author_sort_edit)
self.tags_view.tag_item_renamed.connect(self.do_tag_item_renamed)
@@ -1315,6 +1362,18 @@ class TagBrowserMixin(object): # {{{
self.edit_categories.clicked.connect(lambda x:
self.do_user_categories_edit())
+ def do_add_subcategory(self, on_category=None):
+ db = self.library_view.model().db
+ user_cats = db.prefs.get('user_categories', {})
+ new_cat = on_category[1:] + '.New Category'
+ user_cats[new_cat] = []
+ db.prefs.set('user_categories', user_cats)
+ self.tags_view.set_new_model()
+ m = self.tags_view.model()
+ idx = m.index_for_path(m.find_category_node('@' + new_cat))
+ m.show_item_at_index(idx)
+ self.tags_view.edit(idx)
+
def do_user_categories_edit(self, on_category=None):
db = self.library_view.model().db
d = TagCategories(self, db, on_category)
From 06ca716b26d402837d43c7cbda11f2d341eb681b Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Wed, 23 Feb 2011 14:30:54 +0000
Subject: [PATCH 18/55] More tag browser documentation
---
src/calibre/manual/gui.rst | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/calibre/manual/gui.rst b/src/calibre/manual/gui.rst
index 67d67c6383..d2de87fa91 100644
--- a/src/calibre/manual/gui.rst
+++ b/src/calibre/manual/gui.rst
@@ -425,6 +425,13 @@ There is a search bar at the top of the Tag Browser that allows you to easily fi
For convenience, you can drag and drop books from the book list to items in the Tag Browser and that item will be automatically applied to the dropped books. For example, dragging a book to Isaac Asimov will set the author of that book to Isaac Asimov or dragging it to the tag History will add the tag History to its tags.
+The outer-level items in the tag browser such as Authors and Series are called categories. You can create your own categories, called User Categories, which are useful for organizing items. For example, you can use the user categories editor (push the Manage User Categories button) to create a user category called Favorite Authors, then put the items for your favorites into the category. User categories act like built-in categories; you can click on items to search for them. You can search for all items in a category by right-clicking on the category name and choosing "Search for books in ...".
+
+User categories can have sub-categories. For example, the user category Favorites.Authors is a sub-category of Favorites. You might also have Favorites.Series, in which case there will be two sub-categories under Favorites. Sub-categories can be created using Manage User Categories by entering names like the Favorites example. They can also be created by right-clicking on a user category, choosing "Add sub-category to ...", and entering the category name.
+
+It is also possible to create hierarchies inside some of the built-in categories (the text categories). These hierarchies show with the small triangle permitting the sub-items to be hidden. To use hierarchies in a category, you must first go to Preferences / Look & Feel and enter the category name(s) into the "Categories with hierarchical items" box. Once this is done, items in that category that contain periods will be shown using the small triangle. For example, assume you create a custom column called Genre and indicate that it contains hierarchical items. Once done, items such as Mystery.Thriller and Mystery.English will display as Mystery with the small triangle next to it. Clicking on the triangle will show Thriller and English as sub-items.
+
+
Jobs
-----
.. image:: images/jobs.png
From a71599ba7b37f7fda5878d6a36f62ad3e156bf63 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Wed, 23 Feb 2011 14:46:37 +0000
Subject: [PATCH 19/55] Two bugs in the new categorization stuff: 1) if a user
created a new user category but didn't rename it, the next create would throw
an exception 2) it was possible to specify that invalid fields such as author
contain hierarchies
---
src/calibre/gui2/tag_view.py | 29 ++++++++++++++++++++++-------
1 file changed, 22 insertions(+), 7 deletions(-)
diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py
index 733662c7ec..6cf1eaa448 100644
--- a/src/calibre/gui2/tag_view.py
+++ b/src/calibre/gui2/tag_view.py
@@ -880,11 +880,12 @@ class TagsModel(QAbstractItemModel): # {{{
if cat_len <= 0:
return ((collapse_letter, collapse_letter_sk))
+ fm = self.db.field_metadata[key]
clear_rating = True if key not in self.categories_with_ratings and \
- not self.db.field_metadata[key]['is_custom'] and \
- not self.db.field_metadata[key]['kind'] == 'user' \
+ not fm['is_custom'] and \
+ not fm['kind'] == 'user' \
else False
- tt = key if self.db.field_metadata[key]['kind'] == 'user' else None
+ tt = key if fm['kind'] == 'user' else None
for idx,tag in enumerate(data[key]):
if clear_rating:
tag.avg_rating = None
@@ -931,9 +932,11 @@ class TagsModel(QAbstractItemModel): # {{{
node_parent = category
components = [t for t in tag.name.split('.')]
- if key not in self.db.prefs.get('categories_using_hierarchy', []) \
- or len(components) == 1 or \
- self.db.field_metadata[key]['kind'] == 'user':
+ if key in ['authors', 'publisher', 'news', 'formats'] or \
+ key not in self.db.prefs.get('categories_using_hierarchy', []) or\
+ len(components) == 1 or \
+ fm['kind'] == 'user' or \
+ fm['datatype'] not in ['text', 'series', 'enumeration']:
self.beginInsertRows(category_index, 999999, 1)
TagTreeItem(parent=node_parent, data=tag, tooltip=tt,
icon_map=self.icon_state_map)
@@ -1365,13 +1368,25 @@ class TagBrowserMixin(object): # {{{
def do_add_subcategory(self, on_category=None):
db = self.library_view.model().db
user_cats = db.prefs.get('user_categories', {})
- new_cat = on_category[1:] + '.' + _('New Category').replace('.', '')
+
+ # Ensure that the temporary name we will use is not already there
+ i = 0
+ new_name = _('New Category').replace('.', '')
+ n = new_name
+ while True:
+ new_cat = on_category[1:] + '.' + n
+ if new_cat not in user_cats:
+ break
+ i += 1
+ n = new_name + unicode(i)
+ # Add the new category
user_cats[new_cat] = []
db.prefs.set('user_categories', user_cats)
self.tags_view.set_new_model()
m = self.tags_view.model()
idx = m.index_for_path(m.find_category_node('@' + new_cat))
m.show_item_at_index(idx)
+ # Open the editor on the new item to rename it
self.tags_view.edit(idx)
def do_user_categories_edit(self, on_category=None):
From a64f4aeb0dcb50f426c2f9085fd795ca6ba07afc Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 23 Feb 2011 07:49:48 -0700
Subject: [PATCH 20/55] Skeleton for D&D in Tag Browser
---
src/calibre/gui2/tag_view.py | 41 ++++++++++++++++++++++++++----------
1 file changed, 30 insertions(+), 11 deletions(-)
diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py
index 733662c7ec..5f34da9ecf 100644
--- a/src/calibre/gui2/tag_view.py
+++ b/src/calibre/gui2/tag_view.py
@@ -7,7 +7,7 @@ __docformat__ = 'restructuredtext en'
Browsing book collection by tags.
'''
-import traceback, copy
+import traceback, copy, cPickle
from itertools import izip
from functools import partial
@@ -16,7 +16,7 @@ from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QFont, QSize, \
QIcon, QPoint, QVBoxLayout, QHBoxLayout, QComboBox, QTimer,\
QAbstractItemModel, QVariant, QModelIndex, QMenu, QFrame,\
QPushButton, QWidget, QItemDelegate, QString, QLabel, \
- QShortcut, QKeySequence, SIGNAL
+ QShortcut, QKeySequence, SIGNAL, QMimeData
from calibre.ebooks.metadata import title_sort
from calibre.gui2 import config, NONE, gprefs
@@ -95,7 +95,8 @@ class TagsView(QTreeView): # {{{
self.setItemDelegate(TagDelegate(self))
self.made_connections = False
self.setAcceptDrops(True)
- self.setDragDropMode(self.DropOnly)
+ self.setDragEnabled(True)
+ self.setDragDropMode(self.DragDrop)
self.setDropIndicatorShown(True)
self.setAutoExpandDelay(500)
self.pane_is_visible = False
@@ -406,12 +407,13 @@ class TagsView(QTreeView): # {{{
fm_dest = self.db.metadata_for_field(item.category_key)
if fm_dest['kind'] == 'user':
md = event.mimeData()
- fm_src = self.db.metadata_for_field(md.column_name)
- if md.column_name in ['authors', 'publisher', 'series'] or \
- (fm_src['is_custom'] and
- fm_src['datatype'] in ['series', 'text'] and
- not fm_src['is_multiple']):
- self.setDropIndicatorShown(True)
+ if hasattr(md, 'column_name'):
+ fm_src = self.db.metadata_for_field(md.column_name)
+ if md.column_name in ['authors', 'publisher', 'series'] or \
+ (fm_src['is_custom'] and
+ fm_src['datatype'] in ['series', 'text'] and
+ not fm_src['is_multiple']):
+ self.setDropIndicatorShown(True)
def clear(self):
if self.model():
@@ -664,10 +666,26 @@ class TagsModel(QAbstractItemModel): # {{{
self.db = self.root_item = None
def mimeTypes(self):
- return ["application/calibre+from_library"]
+ return ["application/calibre+from_library",
+ 'application/calibre+from_tag_browser']
+
+ def mimeData(self, indexes):
+ data = []
+ for idx in indexes:
+ if idx.isValid():
+ # get some useful serializable data
+ name = unicode(self.data(idx, Qt.DisplayRole).toString())
+ data.append(name)
+ else:
+ data.append(None)
+ raw = bytearray(cPickle.dumps(data, -1))
+ ans = QMimeData()
+ ans.setData('application/calibre+from_tag_browser', raw)
+ return ans
def dropMimeData(self, md, action, row, column, parent):
- if not md.hasFormat("application/calibre+from_library") or \
+ fmts = set([unicode(x) for x in md.formats()])
+ if not fmts.intersection(set(self.mimeTypes())) or \
action != Qt.CopyAction:
return False
idx = parent
@@ -1081,6 +1099,7 @@ class TagsModel(QAbstractItemModel): # {{{
if index.isValid():
node = self.data(index, Qt.UserRole)
if node.type == TagTreeItem.TAG:
+ ans |= Qt.ItemIsDragEnabled
fm = self.db.metadata_for_field(node.tag.category)
if node.tag.category in \
('tags', 'series', 'authors', 'rating', 'publisher') or \
From 7839e164c43b3143c186a34c83292062d8c778d0 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 23 Feb 2011 08:25:08 -0700
Subject: [PATCH 21/55] ...
---
src/calibre/customize/ui.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/calibre/customize/ui.py b/src/calibre/customize/ui.py
index e9feacc67e..0f5508a89e 100644
--- a/src/calibre/customize/ui.py
+++ b/src/calibre/customize/ui.py
@@ -583,7 +583,7 @@ def main(args=sys.argv):
if remove_plugin(opts.remove_plugin):
print 'Plugin removed'
else:
- print 'No custom pluginnamed', opts.remove_plugin
+ print 'No custom plugin named', opts.remove_plugin
if opts.customize_plugin is not None:
name, custom = opts.customize_plugin.split(',')
plugin = find_plugin(name.strip())
From 9fdfe8311b659c3e75e42a4044fee8ee71ccab3e Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Wed, 23 Feb 2011 16:57:56 +0000
Subject: [PATCH 22/55] Drag tag browser items & drop on user categories.
---
src/calibre/gui2/tag_view.py | 69 +++++++++++++++++++++++++++++++++---
1 file changed, 65 insertions(+), 4 deletions(-)
diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py
index 30fa958ecb..378d22d0f6 100644
--- a/src/calibre/gui2/tag_view.py
+++ b/src/calibre/gui2/tag_view.py
@@ -398,14 +398,18 @@ class TagsView(QTreeView): # {{{
index = self.indexAt(event.pos())
if not index.isValid():
return
+ src_is_tb = event.mimeData().hasFormat('application/calibre+from_tag_browser')
item = index.internalPointer()
flags = self._model.flags(index)
if item.type == TagTreeItem.TAG and flags & Qt.ItemIsDropEnabled:
- self.setDropIndicatorShown(True)
+ self.setDropIndicatorShown(not src_is_tb)
else:
if item.type == TagTreeItem.CATEGORY:
fm_dest = self.db.metadata_for_field(item.category_key)
if fm_dest['kind'] == 'user':
+ if src_is_tb:
+ self.setDropIndicatorShown(True)
+ return
md = event.mimeData()
if hasattr(md, 'column_name'):
fm_src = self.db.metadata_for_field(md.column_name)
@@ -674,8 +678,17 @@ class TagsModel(QAbstractItemModel): # {{{
for idx in indexes:
if idx.isValid():
# get some useful serializable data
- name = unicode(self.data(idx, Qt.DisplayRole).toString())
- data.append(name)
+ node = idx.internalPointer()
+ if node.type == TagTreeItem.CATEGORY:
+ d = (node.type, node.py_name, node.category_key)
+ else:
+ t = node.tag
+ p = node
+ while p.type != TagTreeItem.CATEGORY:
+ p = p.parent
+ d = (node.type, p.category_key,
+ getattr(t, 'original_name', t.name), t.category, t.id)
+ data.append(d)
else:
data.append(None)
raw = bytearray(cPickle.dumps(data, -1))
@@ -688,6 +701,53 @@ class TagsModel(QAbstractItemModel): # {{{
if not fmts.intersection(set(self.mimeTypes())) or \
action != Qt.CopyAction:
return False
+ if "application/calibre+from_library" in fmts:
+ return self.do_drop_from_library(md, action, row, column, parent)
+ elif 'application/calibre+from_tag_browser' in fmts:
+ return self.do_drop_from_tag_browser(md, action, row, column, parent)
+
+ def do_drop_from_tag_browser(self, md, action, row, column, parent):
+ if not parent.isValid():
+ return False
+ dest = parent.internalPointer()
+ if dest.type != TagTreeItem.CATEGORY:
+ return False
+ if not md.hasFormat('application/calibre+from_tag_browser'):
+ return False
+ data = str(md.data('application/calibre+from_tag_browser'))
+ src = cPickle.loads(data)
+ for s in src:
+ if s[0] != TagTreeItem.TAG:
+ return False
+ user_cats = self.db.prefs.get('user_categories', {})
+ for s in src:
+ src_parent, src_name, src_cat = s[1:4]
+ src_parent = src_parent[1:]
+ dest_key = dest.category_key[1:]
+ if dest_key not in user_cats:
+ continue
+ new_cat = []
+ # delete the item if the source is a user category
+ if src_parent in user_cats:
+ for tup in user_cats[src_parent]:
+ if src_name == tup[0] and src_cat == tup[1]:
+ continue
+ new_cat.append(list(tup))
+ user_cats[src_parent] = new_cat
+ # Now add the item to the destination user category
+ add_it = True
+ for tup in user_cats[dest_key]:
+ if src_name == tup[0] and src_cat == tup[1]:
+ add_it = False
+ if add_it:
+ user_cats[dest_key].append([src_name, src_cat, 0])
+ self.db.prefs.set('user_categories', user_cats)
+ path = self.path_for_index(parent)
+ self.tags_view.set_new_model()
+ self.tags_view.model().show_item_at_path(path)
+ return True
+
+ def do_drop_from_library(self, md, action, row, column, parent):
idx = parent
if idx.isValid():
node = self.data(idx, Qt.UserRole)
@@ -1102,7 +1162,8 @@ class TagsModel(QAbstractItemModel): # {{{
if index.isValid():
node = self.data(index, Qt.UserRole)
if node.type == TagTreeItem.TAG:
- ans |= Qt.ItemIsDragEnabled
+ if getattr(node.tag, 'can_edit', True):
+ ans |= Qt.ItemIsDragEnabled
fm = self.db.metadata_for_field(node.tag.category)
if node.tag.category in \
('tags', 'series', 'authors', 'rating', 'publisher') or \
From 418d10eace45d228804a3c8b731da2c532180666 Mon Sep 17 00:00:00 2001
From: Kovid Goyal