Columns preferences widget implemented

This commit is contained in:
Kovid Goyal 2010-09-03 15:11:01 -06:00
parent e497d0d1c4
commit 97bccd23c6
5 changed files with 549 additions and 3 deletions

View File

@ -694,7 +694,16 @@ class Behavior(PreferencesPlugin):
name_order = 2 name_order = 2
config_widget = 'calibre.gui2.preferences.behavior' config_widget = 'calibre.gui2.preferences.behavior'
plugins += [LookAndFeel, Behavior] class Columns(PreferencesPlugin):
name = 'Custom Columns'
gui_name = _('Add your own columns')
category = 'Interface'
gui_category = _('Interface')
category_order = 1
name_order = 3
config_widget = 'calibre.gui2.preferences.columns'
plugins += [LookAndFeel, Behavior, Columns]
#}}} #}}}

View File

@ -0,0 +1,172 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import copy, sys
from PyQt4.Qt import Qt, QVariant, QListWidgetItem
from calibre.gui2.preferences import ConfigWidgetBase, test_widget
from calibre.gui2.preferences.columns_ui import Ui_Form
from calibre.gui2.preferences.create_custom_column import CreateCustomColumn
from calibre.gui2 import error_dialog, question_dialog, ALL_COLUMNS
class ConfigWidget(ConfigWidgetBase, Ui_Form):
def genesis(self, gui):
self.gui = gui
db = self.gui.library_view.model().db
self.custcols = copy.deepcopy(db.field_metadata.get_custom_field_metadata())
self.column_up.clicked.connect(self.up_column)
self.column_down.clicked.connect(self.down_column)
self.del_custcol_button.clicked.connect(self.del_custcol)
self.add_custcol_button.clicked.connect(self.add_custcol)
self.add_col_button.clicked.connect(self.add_custcol)
self.edit_custcol_button.clicked.connect(self.edit_custcol)
for signal in ('Activated', 'Changed', 'DoubleClicked', 'Clicked'):
signal = getattr(self.opt_columns, 'item'+signal)
signal.connect(self.columns_changed)
def initialize(self):
ConfigWidgetBase.initialize(self)
self.init_columns()
def restore_defaults(self):
ConfigWidgetBase.restore_defaults(self)
self.init_columns(defaults=True)
self.changed_signal.emit()
def commit(self):
rr = ConfigWidgetBase.commit(self)
return self.apply_custom_column_changes() or rr
def columns_changed(self, *args):
self.changed_signal.emit()
def columns_state(self, defaults=False):
if defaults:
return self.gui.library_view.get_default_state()
return self.gui.library_view.get_state()
def init_columns(self, defaults=False):
# Set up columns
self.opt_columns.blockSignals(True)
model = self.gui.library_view.model()
colmap = list(model.column_map)
state = self.columns_state(defaults)
hidden_cols = state['hidden_columns']
positions = state['column_positions']
colmap.sort(cmp=lambda x,y: cmp(positions[x], positions[y]))
self.opt_columns.clear()
for col in colmap:
item = QListWidgetItem(model.headers[col], self.opt_columns)
item.setData(Qt.UserRole, QVariant(col))
flags = Qt.ItemIsEnabled|Qt.ItemIsSelectable
if col != 'ondevice':
flags |= Qt.ItemIsUserCheckable
item.setFlags(flags)
if col != 'ondevice':
item.setCheckState(Qt.Unchecked if col in hidden_cols else
Qt.Checked)
self.opt_columns.blockSignals(False)
def up_column(self):
idx = self.opt_columns.currentRow()
if idx > 0:
self.opt_columns.insertItem(idx-1, self.opt_columns.takeItem(idx))
self.opt_columns.setCurrentRow(idx-1)
self.changed_signal.emit()
def down_column(self):
idx = self.opt_columns.currentRow()
if idx < self.opt_columns.count()-1:
self.opt_columns.insertItem(idx+1, self.opt_columns.takeItem(idx))
self.opt_columns.setCurrentRow(idx+1)
self.changed_signal.emit()
def del_custcol(self):
idx = self.opt_columns.currentRow()
if idx < 0:
return error_dialog(self, '', _('You must select a column to delete it'),
show=True)
col = unicode(self.opt_columns.item(idx).data(Qt.UserRole).toString())
if col not in self.custcols:
return error_dialog(self, '',
_('The selected column is not a custom column'), show=True)
if not question_dialog(self, _('Are you sure?'),
_('Do you really want to delete column %s and all its data?') %
self.custcols[col]['name'], show_copy_button=False):
return
self.opt_columns.item(idx).setCheckState(False)
self.opt_columns.takeItem(idx)
self.custcols[col]['*deleteme'] = True
self.changed_signal.emit()
def add_custcol(self):
model = self.gui.library_view.model()
CreateCustomColumn(self, False, model.orig_headers, ALL_COLUMNS)
self.changed_signal.emit()
def edit_custcol(self):
model = self.gui.library_view.model()
CreateCustomColumn(self, True, model.orig_headers, ALL_COLUMNS)
self.changed_signal.emit()
def apply_custom_column_changes(self):
model = self.gui.library_view.model()
db = model.db
config_cols = [unicode(self.opt_columns.item(i).data(Qt.UserRole).toString())\
for i in range(self.opt_columns.count())]
if not config_cols:
config_cols = ['title']
removed_cols = set(model.column_map) - set(config_cols)
hidden_cols = set([unicode(self.opt_columns.item(i).data(Qt.UserRole).toString())\
for i in range(self.opt_columns.count()) \
if self.opt_columns.item(i).checkState()==Qt.Unchecked])
hidden_cols = hidden_cols.union(removed_cols) # Hide removed cols
hidden_cols = list(hidden_cols.intersection(set(model.column_map)))
if 'ondevice' in hidden_cols:
hidden_cols.remove('ondevice')
def col_pos(x, y):
xidx = config_cols.index(x) if x in config_cols else sys.maxint
yidx = config_cols.index(y) if y in config_cols else sys.maxint
return cmp(xidx, yidx)
positions = {}
for i, col in enumerate((sorted(model.column_map, cmp=col_pos))):
positions[col] = i
state = {'hidden_columns': hidden_cols, 'column_positions':positions}
self.gui.library_view.apply_state(state)
self.gui.library_view.save_state()
must_restart = False
for c in self.custcols:
if self.custcols[c]['colnum'] is None:
db.create_custom_column(
label=self.custcols[c]['label'],
name=self.custcols[c]['name'],
datatype=self.custcols[c]['datatype'],
is_multiple=self.custcols[c]['is_multiple'],
display = self.custcols[c]['display'])
must_restart = True
elif '*deleteme' in self.custcols[c]:
db.delete_custom_column(label=self.custcols[c]['label'])
must_restart = True
elif '*edited' in self.custcols[c]:
cc = self.custcols[c]
db.set_custom_column_metadata(cc['colnum'], name=cc['name'],
label=cc['label'],
display = self.custcols[c]['display'])
if '*must_restart' in self.custcols[c]:
must_restart = True
return must_restart
if __name__ == '__main__':
from PyQt4.Qt import QApplication
app = QApplication([])
test_widget('Interface', 'Custom Columns')

View File

@ -25,7 +25,7 @@
</widget> </widget>
</item> </item>
<item row="1" column="0"> <item row="1" column="0">
<widget class="QListWidget" name="columns"> <widget class="QListWidget" name="opt_columns">
<property name="alternatingRowColors"> <property name="alternatingRowColors">
<bool>true</bool> <bool>true</bool>
</property> </property>
@ -155,7 +155,7 @@
</layout> </layout>
</item> </item>
<item row="2" column="0" colspan="2"> <item row="2" column="0" colspan="2">
<widget class="QPushButton" name="pushButton"> <widget class="QPushButton" name="add_col_button">
<property name="text"> <property name="text">
<string>Add &amp;custom column</string> <string>Add &amp;custom column</string>
</property> </property>

View File

@ -0,0 +1,174 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid at kovidgoyal.net>'
'''Dialog to create a new custom column'''
import re
from functools import partial
from PyQt4.QtCore import SIGNAL
from PyQt4.Qt import QDialog, Qt, QListWidgetItem, QVariant
from calibre.gui2.preferences.create_custom_column_ui import Ui_QCreateCustomColumn
from calibre.gui2 import error_dialog
class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
column_types = {
0:{'datatype':'text',
'text':_('Text, column shown in the tag browser'),
'is_multiple':False},
1:{'datatype':'*text',
'text':_('Comma separated text, like tags, shown in the tag browser'),
'is_multiple':True},
2:{'datatype':'comments',
'text':_('Long text, like comments, not shown in the tag browser'),
'is_multiple':False},
3:{'datatype':'series',
'text':_('Text column for keeping series-like information'),
'is_multiple':False},
4:{'datatype':'datetime',
'text':_('Date'), 'is_multiple':False},
5:{'datatype':'float',
'text':_('Floating point numbers'), 'is_multiple':False},
6:{'datatype':'int',
'text':_('Integers'), 'is_multiple':False},
7:{'datatype':'rating',
'text':_('Ratings, shown with stars'),
'is_multiple':False},
8:{'datatype':'bool',
'text':_('Yes/No'), 'is_multiple':False},
}
def __init__(self, parent, editing, standard_colheads, standard_colnames):
QDialog.__init__(self, parent)
Ui_QCreateCustomColumn.__init__(self)
self.setupUi(self)
# Remove help icon on title bar
icon = self.windowIcon()
self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint))
self.setWindowIcon(icon)
self.simple_error = partial(error_dialog, self, show=True,
show_copy_button=False)
self.connect(self.button_box, SIGNAL("accepted()"), self.accept)
self.connect(self.button_box, SIGNAL("rejected()"), self.reject)
self.parent = parent
self.editing_col = editing
self.standard_colheads = standard_colheads
self.standard_colnames = standard_colnames
for t in self.column_types:
self.column_type_box.addItem(self.column_types[t]['text'])
self.column_type_box.currentIndexChanged.connect(self.datatype_changed)
if not self.editing_col:
self.datatype_changed()
self.exec_()
return
idx = parent.opt_columns.currentRow()
if idx < 0:
self.simple_error(_('No column selected'),
_('No column has been selected'))
return
col = unicode(parent.opt_columns.item(idx).data(Qt.UserRole).toString())
if col not in parent.custcols:
self.simple_error('', _('Selected column is not a user-defined column'))
return
c = parent.custcols[col]
self.column_name_box.setText(c['label'])
self.column_heading_box.setText(c['name'])
ct = c['datatype'] if not c['is_multiple'] else '*text'
self.orig_column_number = c['colnum']
self.orig_column_name = col
column_numbers = dict(map(lambda x:(self.column_types[x]['datatype'], x), self.column_types))
self.column_type_box.setCurrentIndex(column_numbers[ct])
self.column_type_box.setEnabled(False)
if ct == 'datetime':
if c['display'].get('date_format', None):
self.date_format_box.setText(c['display'].get('date_format', ''))
self.datatype_changed()
self.exec_()
def datatype_changed(self, *args):
try:
col_type = self.column_types[self.column_type_box.currentIndex()]['datatype']
except:
col_type = None
df_visible = col_type == 'datetime'
for x in ('box', 'default_label', 'label'):
getattr(self, 'date_format_'+x).setVisible(df_visible)
def accept(self):
col = unicode(self.column_name_box.text())
if not col:
return self.simple_error('', _('No lookup name was provided'))
if re.match('^\w*$', col) is None or not col[0].isalpha() or col.lower() != col:
return self.simple_error('', _('The lookup name must contain only lower case letters, digits and underscores, and start with a letter'))
if col.endswith('_index'):
return self.simple_error('', _('Lookup names cannot end with _index, because these names are reserved for the index of a series column.'))
col_heading = unicode(self.column_heading_box.text())
col_type = self.column_types[self.column_type_box.currentIndex()]['datatype']
if col_type == '*text':
col_type='text'
is_multiple = True
else:
is_multiple = False
if not col_heading:
return self.simple_error('', _('No column heading was provided'))
bad_col = False
if col in self.parent.custcols:
if not self.editing_col or self.parent.custcols[col]['colnum'] != self.orig_column_number:
bad_col = True
if bad_col:
return self.simple_error('', _('The lookup name %s is already used')%col)
bad_head = False
for t in self.parent.custcols:
if self.parent.custcols[t]['name'] == col_heading:
if not self.editing_col or self.parent.custcols[t]['colnum'] != self.orig_column_number:
bad_head = True
for t in self.standard_colheads:
if self.standard_colheads[t] == col_heading:
bad_head = True
if bad_head:
return self.simple_error('', _('The heading %s is already used')%col_heading)
date_format = {}
if col_type == 'datetime':
if self.date_format_box.text():
date_format = {'date_format':unicode(self.date_format_box.text())}
else:
date_format = {'date_format': None}
db = self.parent.gui.library_view.model().db
key = db.field_metadata.custom_field_prefix+col
if not self.editing_col:
db.field_metadata
self.parent.custcols[key] = {
'label':col,
'name':col_heading,
'datatype':col_type,
'editable':True,
'display':date_format,
'normalized':None,
'colnum':None,
'is_multiple':is_multiple,
}
item = QListWidgetItem(col_heading, self.parent.opt_columns)
item.setData(Qt.UserRole, QVariant(key))
item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable|Qt.ItemIsSelectable)
item.setCheckState(Qt.Checked)
else:
idx = self.parent.opt_columns.currentRow()
item = self.parent.opt_columns.item(idx)
item.setData(Qt.UserRole, QVariant(key))
item.setText(col_heading)
self.parent.custcols[self.orig_column_name]['label'] = col
self.parent.custcols[self.orig_column_name]['name'] = col_heading
self.parent.custcols[self.orig_column_name]['display'].update(date_format)
self.parent.custcols[self.orig_column_name]['*edited'] = True
self.parent.custcols[self.orig_column_name]['*must_restart'] = True
QDialog.accept(self)
def reject(self):
QDialog.reject(self)

View File

@ -0,0 +1,191 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>QCreateCustomColumn</class>
<widget class="QDialog" name="QCreateCustomColumn">
<property name="windowModality">
<enum>Qt::ApplicationModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>528</width>
<height>199</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Create or edit custom columns</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QGridLayout" name="gridLayout_2" rowstretch="0,0,0,0">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<property name="margin">
<number>5</number>
</property>
<item row="2" column="0">
<layout class="QGridLayout" name="gridLayout">
<property name="margin">
<number>0</number>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>&amp;Lookup name</string>
</property>
<property name="buddy">
<cstring>column_name_box</cstring>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Column &amp;heading</string>
</property>
<property name="buddy">
<cstring>column_heading_box</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="column_name_box">
<property name="minimumSize">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>Used for searching the column. Must contain only digits and lower case letters.</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="column_heading_box">
<property name="toolTip">
<string>Column heading in the library view and category name in the tag browser</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Column &amp;type</string>
</property>
<property name="buddy">
<cstring>column_type_box</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="column_type_box">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>70</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>What kind of information will be kept in the column.</string>
</property>
</widget>
</item>
<item row="4" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLineEdit" name="date_format_box">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>&lt;p&gt;Date format. Use 1-4 'd's for day, 1-4 'M's for month, and 2 or 4 'y's for year.&lt;/p&gt;
&lt;p&gt;For example:
&lt;ul&gt;
&lt;li&gt; ddd, d MMM yyyy gives Mon, 5 Jan 2010&lt;li&gt;
&lt;li&gt;dd MMMM yy gives 05 January 10&lt;/li&gt;
&lt;/ul&gt; </string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="date_format_default_label">
<property name="toolTip">
<string>Use MMM yyyy for month + year, yyyy for year only</string>
</property>
<property name="text">
<string>Default: dd MMM yyyy.</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="4" column="0">
<widget class="QLabel" name="date_format_label">
<property name="text">
<string>Format for &amp;dates</string>
</property>
<property name="buddy">
<cstring>date_format_box</cstring>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="0">
<widget class="QDialogButtonBox" name="button_box">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
<property name="centerButtons">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_6">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Create or edit custom columns</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<tabstops>
<tabstop>column_name_box</tabstop>
<tabstop>column_heading_box</tabstop>
<tabstop>column_type_box</tabstop>
<tabstop>date_format_box</tabstop>
<tabstop>button_box</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>