Allow using textures for cover grid background

Cover grid: Allow using images as the background for the cover grid. To
choose an image, go to Preferences->Look & Feel->Cover Grid. Fixes #1239194 [[Ideas] -->book grid](https://bugs.launchpad.net/calibre/+bug/1239194) [[Ideas] -->book grid](https://bugs.launchpad.net/calibre/+bug/1239194)

The builtin textures come from subtlepatterns.com
This commit is contained in:
Kovid Goyal 2013-10-15 11:12:51 +05:30
parent 2277ed4b17
commit a43941dc0d
10 changed files with 234 additions and 67 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

View File

@ -118,6 +118,7 @@ defs['cover_grid_color'] = (80, 80, 80)
defs['cover_grid_cache_size'] = 100
defs['cover_grid_disk_cache_size'] = 2500
defs['cover_grid_show_title'] = False
defs['cover_grid_texture'] = None
defs['show_vl_tabs'] = False
del defs
# }}}

View File

@ -19,7 +19,7 @@ from PyQt4.Qt import (
QTimer, QPalette, QColor, QItemSelection, QPixmap, QMenu, QApplication,
QMimeData, QUrl, QDrag, QPoint, QPainter, QRect, pyqtProperty, QEvent,
QPropertyAnimation, QEasingCurve, pyqtSlot, QHelpEvent, QAbstractItemView,
QStyleOptionViewItem, QToolTip, QByteArray, QBuffer)
QStyleOptionViewItem, QToolTip, QByteArray, QBuffer, QBrush)
from calibre import fit_image, prints, prepare_string_for_xml
from calibre.ebooks.metadata import fmt_sidx
@ -586,6 +586,12 @@ class GridView(QListView):
pal = QPalette()
col = QColor(r, g, b)
pal.setColor(pal.Base, col)
tex = gprefs['cover_grid_texture']
if tex:
from calibre.gui2.preferences.texture_chooser import texture_path
path = texture_path(tex)
if path:
pal.setBrush(pal.Base, QBrush(QPixmap(path)))
dark = (r + g + b)/3.0 < 128
pal.setColor(pal.Text, QColor(Qt.white if dark else Qt.black))
self.setPalette(pal)

View File

@ -8,13 +8,15 @@ __docformat__ = 'restructuredtext en'
from threading import Thread
from functools import partial
from PyQt4.Qt import (QApplication, QFont, QFontInfo, QFontDialog, QColorDialog,
QAbstractListModel, Qt, QIcon, QKeySequence, QPalette, QColor, pyqtSignal)
from PyQt4.Qt import (
QApplication, QFont, QFontInfo, QFontDialog, QColorDialog, QPainter,
QAbstractListModel, Qt, QIcon, QKeySequence, QColor, pyqtSignal,
QWidget, QSizePolicy, QBrush, QPixmap, QSize, QPushButton)
from calibre import human_readable
from calibre.gui2.preferences import ConfigWidgetBase, test_widget, CommaSeparatedList
from calibre.gui2.preferences.look_feel_ui import Ui_Form
from calibre.gui2 import config, gprefs, qt_app, NONE, open_local_file
from calibre.gui2 import config, gprefs, qt_app, NONE, open_local_file, question_dialog
from calibre.utils.localization import (available_translations,
get_language, get_lang)
from calibre.utils.config import prefs
@ -98,6 +100,33 @@ class DisplayedFields(QAbstractListModel): # {{{
# }}}
class Background(QWidget): # {{{
def __init__(self, parent):
QWidget.__init__(self, parent)
self.bcol = QColor(*gprefs['cover_grid_color'])
self.btex = gprefs['cover_grid_texture']
self.update_brush()
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
def update_brush(self):
self.brush = QBrush(self.bcol)
if self.btex:
from calibre.gui2.preferences.texture_chooser import texture_path
path = texture_path(self.btex)
if path:
self.brush.setTexture(QPixmap(path))
self.update()
def sizeHint(self):
return QSize(200, 120)
def paintEvent(self, ev):
painter = QPainter(self)
painter.fillRect(ev.rect(), self.brush)
painter.end()
# }}}
class ConfigWidget(ConfigWidgetBase, Ui_Form):
size_calculated = pyqtSignal(object)
@ -209,10 +238,24 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
keys = [unicode(x.toString(QKeySequence.NativeText)) for x in keys]
self.fs_help_msg.setText(unicode(self.fs_help_msg.text())%(
_(' or ').join(keys)))
self.cover_grid_color_button.clicked.connect(self.change_cover_grid_color)
self.cover_grid_default_color_button.clicked.connect(self.restore_cover_grid_color)
self.size_calculated.connect(self.update_cg_cache_size, type=Qt.QueuedConnection)
self.tabWidget.currentChanged.connect(self.tab_changed)
l = self.cg_background_box.layout()
self.cg_bg_widget = w = Background(self)
l.addWidget(w, 0, 0, 3, 1)
self.cover_grid_color_button = b = QPushButton(_('Change &color'), self)
b.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
l.addWidget(b, 0, 1)
b.clicked.connect(self.change_cover_grid_color)
self.cover_grid_texture_button = b = QPushButton(_('Change &background image'), self)
b.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
l.addWidget(b, 1, 1)
b.clicked.connect(self.change_cover_grid_texture)
self.cover_grid_default_appearance_button = b = QPushButton(_('Restore &default appearance'), self)
b.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
l.addWidget(b, 2, 1)
b.clicked.connect(self.restore_cover_grid_appearance)
self.cover_grid_empty_cache.clicked.connect(self.empty_cache)
self.cover_grid_open_cache.clicked.connect(self.open_cg_cache)
self.cover_grid_smaller_cover.clicked.connect(partial(self.resize_cover, True))
@ -270,6 +313,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.edit_rules.initialize(db.field_metadata, db.prefs, mi, 'column_color_rules')
self.icon_rules.initialize(db.field_metadata, db.prefs, mi, 'column_icon_rules')
self.set_cg_color(gprefs['cover_grid_color'])
self.set_cg_texture(gprefs['cover_grid_texture'])
self.update_aspect_ratio()
def open_cg_cache(self):
@ -292,9 +336,12 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.size_calculated.emit(self.gui.grid_view.thumbnail_cache.current_size)
def set_cg_color(self, val):
pal = QPalette()
pal.setColor(QPalette.Window, QColor(*val))
self.cover_grid_color_label.setPalette(pal)
self.cg_bg_widget.bcol = QColor(*val)
self.cg_bg_widget.update_brush()
def set_cg_texture(self, val):
self.cg_bg_widget.btex = val
self.cg_bg_widget.update_brush()
def empty_cache(self):
self.gui.grid_view.thumbnail_cache.empty()
@ -312,17 +359,32 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.icon_rules.clear()
self.changed_signal.emit()
self.set_cg_color(gprefs.defaults['cover_grid_color'])
self.set_cg_texture(gprefs.defaults['cover_grid_texture'])
def change_cover_grid_color(self):
col = QColorDialog.getColor(self.cover_grid_color_label.palette().color(QPalette.Window),
col = QColorDialog.getColor(self.cg_bg_widget.bcol,
self.gui, _('Choose background color for cover grid'))
if col.isValid():
col = tuple(col.getRgb())[:3]
self.set_cg_color(col)
self.changed_signal.emit()
if self.cg_bg_widget.btex:
if question_dialog(
self, _('Remove background image?'),
_('There is currently a background image set, so the color'
' you have chosen will not be visible. Remove the background image?')):
self.set_cg_texture(None)
def restore_cover_grid_color(self):
def change_cover_grid_texture(self):
from calibre.gui2.preferences.texture_chooser import TextureChooser
d = TextureChooser(parent=self, initial=self.cg_bg_widget.btex)
if d.exec_() == d.Accepted:
self.set_cg_texture(d.texture)
self.changed_signal.emit()
def restore_cover_grid_appearance(self):
self.set_cg_color(gprefs.defaults['cover_grid_color'])
self.set_cg_texture(gprefs.defaults['cover_grid_texture'])
self.changed_signal.emit()
def build_font_obj(self):
@ -383,7 +445,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.display_model.commit()
self.edit_rules.commit(self.gui.current_db.prefs)
self.icon_rules.commit(self.gui.current_db.prefs)
gprefs['cover_grid_color'] = tuple(self.cover_grid_color_label.palette().color(QPalette.Window).getRgb())[:3]
gprefs['cover_grid_color'] = tuple(self.cg_bg_widget.bcol.getRgb())[:3]
gprefs['cover_grid_texture'] = self.cg_bg_widget.btex
return rr
def refresh_gui(self, gui):

View File

@ -312,61 +312,12 @@
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label_14">
<property name="text">
<string>Background color for the cover grid:</string>
</property>
<property name="buddy">
<cstring>cover_grid_color_button</cstring>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="cover_grid_color_label">
<property name="minimumSize">
<size>
<width>50</width>
<height>50</height>
</size>
</property>
<property name="autoFillBackground">
<bool>true</bool>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cover_grid_color_button">
<property name="text">
<string>Change &amp;color</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cover_grid_default_color_button">
<property name="text">
<string>Restore &amp;default color</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
<widget class="QGroupBox" name="cg_background_box">
<property name="title">
<string>Background for the cover grid</string>
</property>
<layout class="QGridLayout" name="gridLayout_5"/>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_4">

View File

@ -0,0 +1,146 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
import glob, os, string, shutil
from functools import partial
from PyQt4.Qt import (
QDialog, QVBoxLayout, QListWidget, QListWidgetItem, Qt, QIcon,
QApplication, QSize, QPixmap, QDialogButtonBox, QTimer)
from calibre.constants import config_dir
from calibre.gui2 import choose_files, error_dialog
from calibre.utils.icu import sort_key
def texture_dir():
ans = os.path.join(config_dir, 'textures')
if not os.path.exists(ans):
os.makedirs(ans)
return ans
def texture_path(fname):
if not fname:
return
if fname.startswith(':'):
return I('textures/%s' % fname[1:])
return os.path.join(texture_dir(), fname)
class TextureChooser(QDialog):
def __init__(self, parent=None, initial=None):
QDialog.__init__(self, parent)
self.setWindowTitle(_('Choose a texture'))
self.l = l = QVBoxLayout()
self.setLayout(l)
self.tdir = texture_dir()
self.images = il = QListWidget(self)
il.itemDoubleClicked.connect(self.accept, type=Qt.QueuedConnection)
il.setIconSize(QSize(256, 256))
il.setViewMode(il.IconMode)
il.setFlow(il.LeftToRight)
il.setSpacing(20)
il.setSelectionMode(il.SingleSelection)
il.itemSelectionChanged.connect(self.update_remove_state)
l.addWidget(il)
self.bb = bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel)
bb.accepted.connect(self.accept)
bb.rejected.connect(self.reject)
b = self.add_button = bb.addButton(_('Add texture'), bb.ActionRole)
b.setIcon(QIcon(I('plus.png')))
b.clicked.connect(self.add_texture)
b = self.remove_button = bb.addButton(_('Remove texture'), bb.ActionRole)
b.setIcon(QIcon(I('minus.png')))
b.clicked.connect(self.remove_texture)
l.addWidget(bb)
images = [{
'fname': ':'+os.path.basename(x),
'path': x,
'name': ' '.join(map(string.capitalize, os.path.splitext(os.path.basename(x))[0].split('_')))
} for x in glob.glob(I('textures/*.png'))] + [{
'fname': os.path.basename(x),
'path': x,
'name': os.path.splitext(os.path.basename(x))[0],
} for x in glob.glob(os.path.join(self.tdir, '*')) if x.rpartition('.')[-1].lower() in {'jpeg', 'png', 'jpg'}]
images.sort(key=lambda x:sort_key(x['name']))
map(self.create_item, images)
self.update_remove_state()
if initial:
existing = {unicode(i.data(Qt.UserRole).toString()):i for i in (self.images.item(c) for c in xrange(self.images.count()))}
item = existing.get(initial, None)
if item is not None:
item.setSelected(True)
QTimer.singleShot(100, partial(il.scrollToItem, item))
self.resize(QSize(950, 650))
def create_item(self, data):
x = data
i = QListWidgetItem(QIcon(QPixmap(x['path']).scaled(256, 256, transformMode=Qt.SmoothTransformation)), x['name'], self.images)
i.setData(Qt.UserRole, x['fname'])
i.setData(Qt.UserRole+1, x['path'])
return i
def update_remove_state(self):
removeable = bool(self.selected_fname and not self.selected_fname.startswith(':'))
self.remove_button.setEnabled(removeable)
@property
def texture(self):
return self.selected_fname
def add_texture(self):
path = choose_files(self, 'choose-texture-image', _('Choose Image'),
filters=[(_('Images'), ['jpeg', 'jpg', 'png'])], all_files=False, select_only_single_file=True)
if not path:
return
path = path[0]
fname = os.path.basename(path)
name = fname.rpartition('.')[0]
existing = {unicode(i.data(Qt.UserRole).toString()):i for i in (self.images.item(c) for c in xrange(self.images.count()))}
dest = os.path.join(self.tdir, fname)
with open(path, 'rb') as s, open(dest, 'wb') as f:
shutil.copyfileobj(s, f)
if fname in existing:
self.takeItem(existing[fname])
data = {'fname': fname, 'path': dest, 'name': name}
i = self.create_item(data)
i.setSelected(True)
self.images.scrollToItem(i)
@property
def selected_item(self):
for x in self.images.selectedItems():
return x
@property
def selected_fname(self):
try:
return unicode(self.selected_item.data(Qt.UserRole).toString())
except (AttributeError, TypeError):
pass
def remove_texture(self):
if not self.selected_fname:
return
if self.selected_fname.startswith(':'):
return error_dialog(self, _('Cannot remove'),
_('Cannot remover builtin textures'), show=True)
self.images.takeItem(self.images.row(self.selected_item))
if __name__ == '__main__':
app = QApplication([]) # noqa
d = TextureChooser()
d.exec_()
print (d.texture)