mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 10:44:09 -04:00
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:
parent
2277ed4b17
commit
a43941dc0d
BIN
resources/images/textures/dark_cloth.png
Normal file
BIN
resources/images/textures/dark_cloth.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
BIN
resources/images/textures/dark_wood.png
Normal file
BIN
resources/images/textures/dark_wood.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 132 KiB |
BIN
resources/images/textures/grey_wash_wall.png
Normal file
BIN
resources/images/textures/grey_wash_wall.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 46 KiB |
BIN
resources/images/textures/light_wood.png
Normal file
BIN
resources/images/textures/light_wood.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 168 KiB |
BIN
resources/images/textures/subtle_wood.png
Normal file
BIN
resources/images/textures/subtle_wood.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 100 KiB |
@ -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
|
||||
# }}}
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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 &color</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="cover_grid_default_color_button">
|
||||
<property name="text">
|
||||
<string>Restore &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">
|
||||
|
146
src/calibre/gui2/preferences/texture_chooser.py
Normal file
146
src/calibre/gui2/preferences/texture_chooser.py
Normal 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)
|
Loading…
x
Reference in New Issue
Block a user