gramps/src/plugins/docgen/GtkPrint.py

620 lines
21 KiB
Python

#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2007 Zsolt Foldvari
# Copyright (C) 2008-2009 Brian G. Matherly
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# $Id$
"""Printing interface based on gtk.Print* classes.
"""
#------------------------------------------------------------------------
#
# Python modules
#
#------------------------------------------------------------------------
from gettext import gettext as _
from math import radians
##try:
##from cStringIO import StringIO
##except:
##from StringIO import StringIO
#------------------------------------------------------------------------
#
# Gramps modules
#
#------------------------------------------------------------------------
import BaseDoc
import libcairodoc
from gen.plug import PluginManager, DocGenPlugin
import Errors
from glade import Glade
#------------------------------------------------------------------------
#
# Set up logging
#
#------------------------------------------------------------------------
import logging
log = logging.getLogger(".GtkPrint")
#-------------------------------------------------------------------------
#
# GTK modules
#
#-------------------------------------------------------------------------
import gtk
import cairo
if gtk.pygtk_version < (2, 10, 0):
raise Errors.UnavailableError(_("PyGtk 2.10 or later is required"))
#------------------------------------------------------------------------
#
# Constants
#
#------------------------------------------------------------------------
# printer settings (might be needed to align for different platforms)
PRINTER_DPI = 72.0
PRINTER_SCALE = 1.0
# the print settings to remember between print sessions
PRINT_SETTINGS = None
# minimum spacing around a page in print preview
MARGIN = 6
# zoom modes in print preview
(ZOOM_BEST_FIT,
ZOOM_FIT_WIDTH,
ZOOM_FREE,) = range(3)
#------------------------------------------------------------------------
#
# Converter functions
#
#------------------------------------------------------------------------
def paperstyle_to_pagesetup(paper_style):
"""Convert a BaseDoc.PaperStyle instance into a gtk.PageSetup instance.
@param paper_style: Gramps paper style object to convert
@param type: BaseDoc.PaperStyle
@return: page_setup
@rtype: gtk.PageSetup
"""
# paper size names according to 'PWG Candidate Standard 5101.1-2002'
# ftp://ftp.pwg.org/pub/pwg/candidates/cs-pwgmsn10-20020226-5101.1.pdf
gramps_to_gtk = {
"Letter": "na_letter",
"Legal": "na_legal",
"A0": "iso_a0",
"A1": "iso_a1",
"A2": "iso_a2",
"A3": "iso_a3",
"A4": "iso_a4",
"A5": "iso_a5",
"B0": "iso_b0",
"B1": "iso_b1",
"B2": "iso_b2",
"B3": "iso_b3",
"B4": "iso_b4",
"B5": "iso_b5",
"B6": "iso_b6",
"B": "na_ledger",
"C": "na_c",
"D": "na_d",
"E": "na_e",
}
# First set the paper size
gramps_paper_size = paper_style.get_size()
gramps_paper_name = gramps_paper_size.get_name()
# All sizes not included in the translation table (even if a standard size)
# are handled as custom format, because we are not intelligent enough.
if gramps_paper_name in gramps_to_gtk:
paper_size = gtk.PaperSize(gramps_to_gtk[gramps_paper_name])
log.debug("Selected paper size: %s" % gramps_to_gtk[gramps_paper_name])
else:
paper_width = gramps_paper_size.get_width() * 10
paper_height = gramps_paper_size.get_height() * 10
paper_size = gtk.paper_size_new_custom("custom",
"Custom Size",
paper_width,
paper_height,
gtk.UNIT_MM)
log.debug("Selected paper size: (%f,%f)" % (paper_width, paper_height))
page_setup = gtk.PageSetup()
page_setup.set_paper_size(paper_size)
# Set paper orientation
if paper_style.get_orientation() == BaseDoc.PAPER_PORTRAIT:
page_setup.set_orientation(gtk.PAGE_ORIENTATION_PORTRAIT)
else:
page_setup.set_orientation(gtk.PAGE_ORIENTATION_LANDSCAPE)
# Set paper margins
page_setup.set_top_margin(paper_style.get_top_margin() * 10,
gtk.UNIT_MM)
page_setup.set_bottom_margin(paper_style.get_bottom_margin() * 10,
gtk.UNIT_MM)
page_setup.set_left_margin(paper_style.get_left_margin() * 10,
gtk.UNIT_MM)
page_setup.set_right_margin(paper_style.get_right_margin() * 10,
gtk.UNIT_MM)
return page_setup
#------------------------------------------------------------------------
#
# PrintPreview class
#
#------------------------------------------------------------------------
class PrintPreview:
"""Implement a dialog to show print preview.
"""
zoom_factors = {
0.50: '50%',
0.75: '75%',
1.00: '100%',
1.25: '125%',
1.50: '150%',
1.75: '175%',
2.00: '200%',
3.00: '300%',
4.00: '400%',
}
def __init__(self, operation, preview, context, parent):
self._operation = operation
self._preview = preview
self._context = context
self.__build_window()
self._current_page = None
# Private
def __build_window(self):
"""Build the window from Glade.
"""
glade_xml = Glade()
self._window = glade_xml.toplevel
#self._window.set_transient_for(parent)
# remember active widgets for future use
self._swin = glade_xml.get_object('swin')
self._drawing_area = glade_xml.get_object('drawingarea')
self._first_button = glade_xml.get_object('first')
self._prev_button = glade_xml.get_object('prev')
self._next_button = glade_xml.get_object('next')
self._last_button = glade_xml.get_object('last')
self._pages_entry = glade_xml.get_object('entry')
self._pages_label = glade_xml.get_object('label')
self._zoom_fit_width_button = glade_xml.get_object('zoom_fit_width')
self._zoom_fit_width_button.set_stock_id('gramps-zoom-fit-width')
self._zoom_best_fit_button = glade_xml.get_object('zoom_best_fit')
self._zoom_best_fit_button.set_stock_id('gramps-zoom-best-fit')
self._zoom_in_button = glade_xml.get_object('zoom_in')
self._zoom_in_button.set_stock_id('gramps-zoom-in')
self._zoom_out_button = glade_xml.get_object('zoom_out')
self._zoom_out_button.set_stock_id('gramps-zoom-out')
# connect the signals
glade_xml.connect_signals(self)
##def create_surface(self):
##return cairo.PDFSurface(StringIO(),
##self._context.get_width(),
##self._context.get_height())
##def get_page(self, page_no):
##"""Get the cairo surface of the given page.
##Surfaces are also cached for instant access.
##"""
##if page_no >= len(self._page_numbers):
##log.debug("Page number %d doesn't exist." % page_no)
##page_no = 0
##if page_no not in self._page_surfaces:
##surface = self.create_surface()
##cr = cairo.Context(surface)
##if PRINTER_SCALE != 1.0:
##cr.scale(PRINTER_SCALE, PRINTER_SCALE)
##self._context.set_cairo_context(cr, PRINTER_DPI, PRINTER_DPI)
##self._preview.render_page(self._page_numbers[page_no])
##self._page_surfaces[page_no] = surface
##return self._page_surfaces[page_no]
def __set_page(self, page_no):
if page_no < 0 or page_no >= self._page_no:
return
if self._current_page != page_no:
self._drawing_area.queue_draw()
self._current_page = page_no
self._first_button.set_sensitive(self._current_page)
self._prev_button.set_sensitive(self._current_page)
self._next_button.set_sensitive(self._current_page < self._page_no - 1)
self._last_button.set_sensitive(self._current_page < self._page_no - 1)
self._pages_entry.set_text('%d' % (self._current_page + 1))
def __set_zoom(self, zoom):
self._zoom = zoom
screen_width = int(self._paper_width * self._zoom + 2 * MARGIN)
screen_height = int(self._paper_height * self._zoom + 2 * MARGIN)
self._drawing_area.set_size_request(screen_width, screen_height)
self._drawing_area.queue_draw()
self._zoom_in_button.set_sensitive(self._zoom !=
max(self.zoom_factors.keys()))
self._zoom_out_button.set_sensitive(self._zoom !=
min(self.zoom_factors.keys()))
def __zoom_in(self):
zoom = [z for z in self.zoom_factors.keys() if z > self._zoom]
if zoom:
return min(zoom)
else:
return self._zoom
def __zoom_out(self):
zoom = [z for z in self.zoom_factors.keys() if z < self._zoom]
if zoom:
return max(zoom)
else:
return self._zoom
def __zoom_fit_width(self):
width, height, vsb_w, hsb_h = self.__get_view_size()
zoom = width / self._paper_width
if self._paper_height * zoom > height:
zoom = (width - vsb_w) / self._paper_width
return zoom
def __zoom_best_fit(self):
width, height, vsb_w, hsb_h = self.__get_view_size()
zoom = min(width / self._paper_width, height / self._paper_height)
return zoom
def __get_view_size(self):
"""Get the dimensions of the scrolled window.
"""
width = self._swin.allocation.width - 2 * MARGIN
height = self._swin.allocation.height - 2 * MARGIN
if self._swin.get_shadow_type() != gtk.SHADOW_NONE:
width -= 2 * self._swin.style.xthickness
height -= 2 * self._swin.style.ythickness
spacing = self._swin.style_get_property('scrollbar-spacing')
vsb_w, vsb_h = self._swin.get_vscrollbar().size_request()
vsb_w += spacing
hsb_w, hsb_h = self._swin.get_hscrollbar().size_request()
hsb_h += spacing
return width, height, vsb_w, hsb_h
def __end_preview(self):
self._operation.end_preview()
# Signal handlers
def on_drawingarea_expose_event(self, drawing_area, event):
cr = drawing_area.window.cairo_create()
cr.rectangle(event.area)
cr.clip()
# get the extents of the page and the screen
paper_w = int(self._paper_width * self._zoom)
paper_h = int(self._paper_height * self._zoom)
width, height, vsb_w, hsb_h = self.__get_view_size()
if paper_h > height:
width -= vsb_w
if paper_w > width:
height -= hsb_h
# put the paper on the middle of the window
xtranslate = MARGIN
if paper_w < width:
xtranslate += (width - paper_w) / 2
ytranslate = MARGIN
if paper_h < height:
ytranslate += (height - paper_h) / 2
cr.translate(xtranslate, ytranslate)
# draw an empty white page
cr.set_source_rgb(1.0, 1.0, 1.0)
cr.rectangle(0, 0, paper_w, paper_h)
cr.fill_preserve()
cr.set_source_rgb(0, 0, 0)
cr.set_line_width(1)
cr.stroke()
if self._orientation == gtk.PAGE_ORIENTATION_LANDSCAPE:
cr.rotate(radians(-90))
cr.translate(-paper_h, 0)
##page_setup = self._context.get_page_setup()
##cr.translate(page_setup.get_left_margin(gtk.UNIT_POINTS),
##page_setup.get_top_margin(gtk.UNIT_POINTS))
##cr.set_source_surface(self.get_page(0))
##cr.paint()
# draw the content of the currently selected page
# Here we use dpi scaling instead of scaling the cairo context,
# because it gives better result. In the latter case the distance
# of glyphs was changing.
dpi = PRINTER_DPI * self._zoom
self._context.set_cairo_context(cr, dpi, dpi)
self._preview.render_page(self._current_page)
def on_swin_size_allocate(self, scrolledwindow, allocation):
if self._zoom_mode == ZOOM_FIT_WIDTH:
self.__set_zoom(self.__zoom_fit_width())
if self._zoom_mode == ZOOM_BEST_FIT:
self.__set_zoom(self.__zoom_best_fit())
def on_print_clicked(self, toolbutton):
pass
def on_first_clicked(self, toolbutton):
self.__set_page(0)
def on_prev_clicked(self, toolbutton):
self.__set_page(self._current_page - 1)
def on_next_clicked(self, toolbutton):
self.__set_page(self._current_page + 1)
def on_last_clicked(self, toolbutton):
self.__set_page(self._page_no - 1)
def on_entry_activate(self, entry):
try:
new_page = int(entry.get_text()) - 1
except ValueError:
new_page = self._current_page
if new_page < 0 or new_page >= self._page_no:
new_page = self._current_page
self.__set_page(new_page)
def on_zoom_fit_width_toggled(self, toggletoolbutton):
if toggletoolbutton.get_active():
self._zoom_best_fit_button.set_active(False)
self._zoom_mode = ZOOM_FIT_WIDTH
self.__set_zoom(self.__zoom_fit_width())
else:
self._zoom_mode = ZOOM_FREE
def on_zoom_best_fit_toggled(self, toggletoolbutton):
if toggletoolbutton.get_active():
self._zoom_fit_width_button.set_active(False)
self._zoom_mode = ZOOM_BEST_FIT
self.__set_zoom(self.__zoom_best_fit())
else:
self._zoom_mode = ZOOM_FREE
def on_zoom_in_clicked(self, toolbutton):
self._zoom_fit_width_button.set_active(False)
self._zoom_best_fit_button.set_active(False)
self._zoom_mode = ZOOM_FREE
self.__set_zoom(self.__zoom_in())
def on_zoom_out_clicked(self, toolbutton):
self._zoom_fit_width_button.set_active(False)
self._zoom_best_fit_button.set_active(False)
self._zoom_mode = ZOOM_FREE
self.__set_zoom(self.__zoom_out())
def on_window_delete_event(self, widget, event):
self.__end_preview()
return False
def on_quit_clicked(self, toolbutton):
self.__end_preview()
self._window.destroy()
# Public
def start(self):
# get paper/page dimensions
page_setup = self._context.get_page_setup()
self._paper_width = page_setup.get_paper_width(gtk.UNIT_POINTS)
self._paper_height = page_setup.get_paper_height(gtk.UNIT_POINTS)
self._page_width = page_setup.get_page_width(gtk.UNIT_POINTS)
self._page_height = page_setup.get_page_height(gtk.UNIT_POINTS)
self._orientation = page_setup.get_orientation()
# get the total number of pages
##self._page_numbers = [0,]
##self._page_surfaces = {}
self._page_no = self._operation.get_property('n_pages')
self._pages_label.set_text(_('of %d') % self._page_no)
# set zoom level and initial page number
self._zoom_mode = ZOOM_FREE
self.__set_zoom(1.0)
self.__set_page(0)
# let's the show begin...
self._window.show()
#------------------------------------------------------------------------
#
# GtkPrint class
#
#------------------------------------------------------------------------
class GtkPrint(libcairodoc.CairoDoc):
"""Print document via GtkPrint* interface.
Requires Gtk+ 2.10.
"""
def run(self):
"""Run the Gtk Print operation.
"""
global PRINT_SETTINGS
# get a page setup from the paper style we have
page_setup = paperstyle_to_pagesetup(self.paper)
# set up a print operation
operation = gtk.PrintOperation()
operation.set_default_page_setup(page_setup)
operation.connect("begin_print", self.on_begin_print)
operation.connect("draw_page", self.on_draw_page)
operation.connect("paginate", self.on_paginate)
operation.connect("preview", self.on_preview)
# set print settings if it was stored previously
if PRINT_SETTINGS is not None:
operation.set_print_settings(PRINT_SETTINGS)
# run print dialog
while True:
self.preview = None
res = operation.run(gtk.PRINT_OPERATION_ACTION_PRINT_DIALOG)
if self.preview is None: # cancel or print
break
# set up printing again; can't reuse PrintOperation?
operation = gtk.PrintOperation()
operation.set_default_page_setup(page_setup)
operation.connect("begin_print", self.on_begin_print)
operation.connect("draw_page", self.on_draw_page)
operation.connect("paginate", self.on_paginate)
operation.connect("preview", self.on_preview)
# set print settings if it was stored previously
if PRINT_SETTINGS is not None:
operation.set_print_settings(PRINT_SETTINGS)
# store print settings if printing was successful
if res == gtk.PRINT_OPERATION_RESULT_APPLY:
PRINT_SETTINGS = operation.get_print_settings()
def on_begin_print(self, operation, context):
"""Setup environment for printing.
"""
# get data from context here only once to save time on pagination
self.page_width = round(context.get_width())
self.page_height = round(context.get_height())
self.dpi_x = context.get_dpi_x()
self.dpi_y = context.get_dpi_y()
def on_paginate(self, operation, context):
"""Paginate the whole document in chunks.
"""
layout = context.create_pango_layout()
finished = self.paginate(layout,
self.page_width,
self.page_height,
self.dpi_x,
self.dpi_y)
# update page number
operation.set_n_pages(len(self._pages))
# start preview if needed
if finished and self.preview:
self.preview.start()
return finished
def on_draw_page(self, operation, context, page_nr):
"""Draw the requested page.
"""
cr = context.get_cairo_context()
layout = context.create_pango_layout()
width = round(context.get_width())
height = round(context.get_height())
dpi_x = context.get_dpi_x()
dpi_y = context.get_dpi_y()
self.draw_page(page_nr, cr, layout, width, height, dpi_x, dpi_y)
def on_preview(self, operation, preview, context, parent):
"""Implement custom print preview functionality.
"""
##if os.sys.platform == 'win32':
##return False
self.preview = PrintPreview(operation, preview, context, parent)
# give a dummy cairo context to gtk.PrintContext,
# PrintPreview will update it with the real one
width = int(round(context.get_width()))
height = int(round(context.get_height()))
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
cr = cairo.Context(surface)
context.set_cairo_context(cr, PRINTER_DPI, PRINTER_DPI)
return True
#------------------------------------------------------------------------
#
# register_plugin
#
#------------------------------------------------------------------------
def register_plugin():
"""
Register the document generator with the GRAMPS plugin system
"""
pmgr = PluginManager.get_instance()
plugin = DocGenPlugin(name = _('Print...'),
description = _("Generates documents and prints them "
"directly."),
basedoc = GtkPrint,
paper = True,
style = True,
extension = "" )
pmgr.register_plugin(plugin)
register_plugin()