diff --git a/resources/images/textures/dark_cloth.png b/resources/images/textures/dark_cloth.png
new file mode 100644
index 0000000000..17a4d6b42d
Binary files /dev/null and b/resources/images/textures/dark_cloth.png differ
diff --git a/resources/images/textures/dark_wood.png b/resources/images/textures/dark_wood.png
new file mode 100644
index 0000000000..6e01b3d6f8
Binary files /dev/null and b/resources/images/textures/dark_wood.png differ
diff --git a/resources/images/textures/grey_wash_wall.png b/resources/images/textures/grey_wash_wall.png
new file mode 100644
index 0000000000..d54e889949
Binary files /dev/null and b/resources/images/textures/grey_wash_wall.png differ
diff --git a/resources/images/textures/light_wood.png b/resources/images/textures/light_wood.png
new file mode 100644
index 0000000000..0b4c35f8c3
Binary files /dev/null and b/resources/images/textures/light_wood.png differ
diff --git a/resources/images/textures/subtle_wood.png b/resources/images/textures/subtle_wood.png
new file mode 100644
index 0000000000..22f2450d3a
Binary files /dev/null and b/resources/images/textures/subtle_wood.png differ
diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py
index 816ed5b617..67d4b80750 100644
--- a/src/calibre/gui2/__init__.py
+++ b/src/calibre/gui2/__init__.py
@@ -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
# }}}
diff --git a/src/calibre/gui2/library/alternate_views.py b/src/calibre/gui2/library/alternate_views.py
index a3885bf135..75178b09a7 100644
--- a/src/calibre/gui2/library/alternate_views.py
+++ b/src/calibre/gui2/library/alternate_views.py
@@ -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)
diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py
index 4fc32b0e77..9e26db4563 100644
--- a/src/calibre/gui2/preferences/look_feel.py
+++ b/src/calibre/gui2/preferences/look_feel.py
@@ -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):
diff --git a/src/calibre/gui2/preferences/look_feel.ui b/src/calibre/gui2/preferences/look_feel.ui
index 774c017698..af09d6d50d 100644
--- a/src/calibre/gui2/preferences/look_feel.ui
+++ b/src/calibre/gui2/preferences/look_feel.ui
@@ -312,61 +312,12 @@
-
-
-
-
-
-
- Background color for the cover grid:
-
-
- cover_grid_color_button
-
-
-
- -
-
-
-
- 50
- 50
-
-
-
- true
-
-
-
-
-
-
- -
-
-
- Change &color
-
-
-
- -
-
-
- Restore &default color
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
-
+
+
+ Background for the cover grid
+
+
+
-
diff --git a/src/calibre/gui2/preferences/texture_chooser.py b/src/calibre/gui2/preferences/texture_chooser.py
new file mode 100644
index 0000000000..c48d274759
--- /dev/null
+++ b/src/calibre/gui2/preferences/texture_chooser.py
@@ -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 '
+
+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)