1# 2# Gramps - a GTK+/GNOME based genealogy program 3# 4# Copyright (C) 2007 Zsolt Foldvari 5# Copyright (C) 2008-2009 Brian G. Matherly 6# 7# This program is free software; you can redistribute it and/or modify 8# it under the terms of the GNU General Public License as published by 9# the Free Software Foundation; either version 2 of the License, or 10# (at your option) any later version. 11# 12# This program is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU General Public License for more details. 16# 17# You should have received a copy of the GNU General Public License 18# along with this program; if not, write to the Free Software 19# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 20# 21 22"""Printing interface based on Gtk.Print* classes. 23""" 24 25#------------------------------------------------------------------------ 26# 27# Python modules 28# 29#------------------------------------------------------------------------ 30from math import radians 31import logging 32 33#------------------------------------------------------------------------- 34# 35# GTK modules 36# 37#------------------------------------------------------------------------- 38import cairo 39try: # the Gramps-Connect server has no DISPLAY 40 from gi.repository import GObject 41 from gi.repository import Gtk 42except: 43 pass 44 45#------------------------------------------------------------------------ 46# 47# Gramps modules 48# 49#------------------------------------------------------------------------ 50from gramps.gen.plug.docgen import PAPER_PORTRAIT 51import gramps.plugins.lib.libcairodoc as libcairodoc 52from gramps.gen.const import GRAMPS_LOCALE as glocale 53_ = glocale.translation.gettext 54 55#------------------------------------------------------------------------ 56# 57# Set up logging 58# 59#------------------------------------------------------------------------ 60LOG = logging.getLogger(".GtkPrint") 61 62#------------------------------------------------------------------------ 63# 64# Constants 65# 66#------------------------------------------------------------------------ 67 68# printer settings (might be needed to align for different platforms) 69PRINTER_DPI = 72.0 70PRINTER_SCALE = 1.0 71 72# the print settings to remember between print sessions 73PRINT_SETTINGS = None 74 75# minimum spacing around a page in print preview 76MARGIN = 6 77 78# zoom modes in print preview 79(ZOOM_BEST_FIT, 80 ZOOM_FIT_WIDTH, 81 ZOOM_FREE,) = list(range(3)) 82 83#------------------------------------------------------------------------ 84# 85# Converter functions 86# 87#------------------------------------------------------------------------ 88 89def paperstyle_to_pagesetup(paper_style): 90 """Convert a PaperStyle instance into a Gtk.PageSetup instance. 91 92 @param paper_style: Gramps paper style object to convert 93 @param type: PaperStyle 94 @return: page_setup 95 @rtype: Gtk.PageSetup 96 """ 97 # paper size names according to 'PWG Candidate Standard 5101.1-2002' 98 # ftp://ftp.pwg.org/pub/pwg/candidates/cs-pwgmsn10-20020226-5101.1.pdf 99 gramps_to_gtk = { 100 "Letter": "na_letter", 101 "Legal": "na_legal", 102 "A0": "iso_a0", 103 "A1": "iso_a1", 104 "A2": "iso_a2", 105 "A3": "iso_a3", 106 "A4": "iso_a4", 107 "A5": "iso_a5", 108 "B0": "iso_b0", 109 "B1": "iso_b1", 110 "B2": "iso_b2", 111 "B3": "iso_b3", 112 "B4": "iso_b4", 113 "B5": "iso_b5", 114 "B6": "iso_b6", 115 "B": "na_ledger", 116 "C": "na_c", 117 "D": "na_d", 118 "E": "na_e", 119 } 120 121 # First set the paper size 122 gramps_paper_size = paper_style.get_size() 123 gramps_paper_name = gramps_paper_size.get_name() 124 125 # All sizes not included in the translation table (even if a standard size) 126 # are handled as custom format, because we are not intelligent enough. 127 if gramps_paper_name in gramps_to_gtk: 128 paper_size = Gtk.PaperSize.new(name=gramps_to_gtk[gramps_paper_name]) 129 LOG.debug("Selected paper size: %s", gramps_to_gtk[gramps_paper_name]) 130 else: 131 if paper_style.get_orientation() == PAPER_PORTRAIT: 132 paper_width = gramps_paper_size.get_width() * 10 133 paper_height = gramps_paper_size.get_height() * 10 134 else: 135 paper_width = gramps_paper_size.get_height() * 10 136 paper_height = gramps_paper_size.get_width() * 10 137 paper_size = Gtk.PaperSize.new_custom("custom", "Custom Size", 138 paper_width, paper_height, 139 Gtk.Unit.MM) 140 LOG.debug("Selected paper size: (%f,%f)", paper_width, paper_height) 141 142 page_setup = Gtk.PageSetup() 143 page_setup.set_paper_size(paper_size) 144 145 # Set paper orientation 146 if paper_style.get_orientation() == PAPER_PORTRAIT: 147 page_setup.set_orientation(Gtk.PageOrientation.PORTRAIT) 148 else: 149 page_setup.set_orientation(Gtk.PageOrientation.LANDSCAPE) 150 151 # Set paper margins 152 page_setup.set_top_margin(paper_style.get_top_margin() * 10, 153 Gtk.Unit.MM) 154 page_setup.set_bottom_margin(paper_style.get_bottom_margin() * 10, 155 Gtk.Unit.MM) 156 page_setup.set_left_margin(paper_style.get_left_margin() * 10, 157 Gtk.Unit.MM) 158 page_setup.set_right_margin(paper_style.get_right_margin() * 10, 159 Gtk.Unit.MM) 160 161 return page_setup 162 163#------------------------------------------------------------------------ 164# 165# PrintPreview class 166# 167#------------------------------------------------------------------------ 168class PrintPreview: 169 """Implement a dialog to show print preview. 170 """ 171 zoom_factors = { 172 0.50: '50%', 173 0.75: '75%', 174 1.00: '100%', 175 1.25: '125%', 176 1.50: '150%', 177 1.75: '175%', 178 2.00: '200%', 179 3.00: '300%', 180 4.00: '400%', 181 } 182 183 def __init__(self, operation, preview, context, parent): 184 self._operation = operation 185 self._preview = preview 186 self._context = context 187 self._parent = parent 188 189 self.__build_window() 190 self._current_page = None 191 192 # Private 193 194 def __build_window(self): 195 """Build the window from Glade. 196 """ 197 from gramps.gui.glade import Glade 198 glade_xml = Glade() 199 self._window = glade_xml.toplevel 200 self._window.set_transient_for(self._parent) 201 202 # remember active widgets for future use 203 self._swin = glade_xml.get_object('swin') 204 self._drawing_area = glade_xml.get_object('drawingarea') 205 self._first_button = glade_xml.get_object('first') 206 self._prev_button = glade_xml.get_object('prev') 207 self._next_button = glade_xml.get_object('next') 208 self._last_button = glade_xml.get_object('last') 209 self._pages_entry = glade_xml.get_object('entry') 210 self._pages_label = glade_xml.get_object('label') 211 self._zoom_fit_width_button = glade_xml.get_object('zoom_fit_width') 212 self._zoom_fit_width_button.set_stock_id('gramps-zoom-fit-width') 213 self._zoom_best_fit_button = glade_xml.get_object('zoom_best_fit') 214 self._zoom_best_fit_button.set_stock_id('gramps-zoom-best-fit') 215 self._zoom_in_button = glade_xml.get_object('zoom_in') 216 self._zoom_in_button.set_stock_id('gramps-zoom-in') 217 self._zoom_out_button = glade_xml.get_object('zoom_out') 218 self._zoom_out_button.set_stock_id('gramps-zoom-out') 219 220 # connect the signals 221 glade_xml.connect_signals(self) 222 self._drawing_area.connect("draw", self.on_drawingarea_draw_event) 223 224 ##def create_surface(self): 225 ##return cairo.PDFSurface(StringIO(), 226 ##self._context.get_width(), 227 ##self._context.get_height()) 228 229 ##def get_page(self, page_no): 230 ##"""Get the cairo surface of the given page. 231 232 ##Surfaces are also cached for instant access. 233 234 ##""" 235 ##if page_no >= len(self._page_numbers): 236 ##LOG.debug("Page number %d doesn't exist." % page_no) 237 ##page_no = 0 238 239 ##if page_no not in self._page_surfaces: 240 ##surface = self.create_surface() 241 ##cr = cairo.Context(surface) 242 243 ##if PRINTER_SCALE != 1.0: 244 ##cr.scale(PRINTER_SCALE, PRINTER_SCALE) 245 246 ##self._context.set_cairo_context(cr, PRINTER_DPI, PRINTER_DPI) 247 ##self._preview.render_page(self._page_numbers[page_no]) 248 249 ##self._page_surfaces[page_no] = surface 250 251 ##return self._page_surfaces[page_no] 252 253 def __set_page(self, page_no): 254 if page_no < 0 or page_no >= self._page_no: 255 return 256 257 if self._current_page != page_no: 258 self._drawing_area.queue_draw() 259 260 self._current_page = page_no 261 262 self._first_button.set_sensitive(self._current_page) 263 self._prev_button.set_sensitive(self._current_page) 264 self._next_button.set_sensitive(self._current_page < self._page_no - 1) 265 self._last_button.set_sensitive(self._current_page < self._page_no - 1) 266 267 self._pages_entry.set_text('%d' % (self._current_page + 1)) 268 269 def __set_zoom(self, zoom): 270 self._zoom = zoom 271 272 screen_width = int(self._paper_width * self._zoom + 2 * MARGIN) 273 screen_height = int(self._paper_height * self._zoom + 2 * MARGIN) 274 self._drawing_area.set_size_request(screen_width, screen_height) 275 self._drawing_area.queue_draw() 276 277 self._zoom_in_button.set_sensitive(self._zoom != 278 max(self.zoom_factors)) 279 self._zoom_out_button.set_sensitive(self._zoom != 280 min(self.zoom_factors)) 281 282 def __zoom_in(self): 283 zoom = [z for z in self.zoom_factors if z > self._zoom] 284 285 if zoom: 286 return min(zoom) 287 else: 288 return self._zoom 289 290 def __zoom_out(self): 291 zoom = [z for z in self.zoom_factors if z < self._zoom] 292 293 if zoom: 294 return max(zoom) 295 else: 296 return self._zoom 297 298 def __zoom_fit_width(self): 299 width, height, vsb_w, hsb_h = self.__get_view_size() 300 301 zoom = width / self._paper_width 302 if self._paper_height * zoom > height: 303 zoom = (width - vsb_w) / self._paper_width 304 305 return zoom 306 307 def __zoom_best_fit(self): 308 width, height, vsb_w, hsb_h = self.__get_view_size() 309 310 zoom = min(width / self._paper_width, height / self._paper_height) 311 312 return zoom 313 314 def __get_view_size(self): 315 """Get the dimensions of the scrolled window. 316 """ 317 width = self._swin.get_allocated_width() - 2 * MARGIN 318 height = self._swin.get_allocated_height() - 2 * MARGIN 319 320 if self._swin.get_shadow_type() != Gtk.ShadowType.NONE: 321 width -= 2 * self._swin.get_style().xthickness 322 height -= 2 * self._swin.get_style().ythickness 323 324 spacing = GObject.Value() 325 spacing.init(GObject.TYPE_INT) 326 spacing = self._swin.style_get_property('scrollbar-spacing', spacing) 327 if spacing: 328 spacing = spacing.get_int() 329 else: 330 spacing = 0 331 332 reqmin, req = self._swin.get_vscrollbar().get_preferred_size() 333 vsb_w = spacing + req.width 334 reqmin, req = self._swin.get_hscrollbar().get_preferred_size() 335 hsb_h = spacing + req.height 336 337 return width, height, vsb_w, hsb_h 338 339 def __end_preview(self): 340 self._operation.end_preview() 341 342 # Signal handlers 343 344 def on_drawingarea_draw_event(self, drawing_area, context): 345 cr = context 346 #cr.rectangle(event.area) 347 #cr.clip() 348 349 # get the extents of the page and the screen 350 paper_w = int(self._paper_width * self._zoom) 351 paper_h = int(self._paper_height * self._zoom) 352 353 width, height, vsb_w, hsb_h = self.__get_view_size() 354 if paper_h > height: 355 width -= vsb_w 356 if paper_w > width: 357 height -= hsb_h 358 359 # put the paper on the middle of the window 360 xtranslate = MARGIN 361 if paper_w < width: 362 xtranslate += (width - paper_w) / 2 363 364 ytranslate = MARGIN 365 if paper_h < height: 366 ytranslate += (height - paper_h) / 2 367 368 cr.translate(xtranslate, ytranslate) 369 370 # draw an empty white page 371 cr.set_source_rgb(1.0, 1.0, 1.0) 372 cr.rectangle(0, 0, paper_w, paper_h) 373 cr.fill_preserve() 374 cr.set_source_rgb(0, 0, 0) 375 cr.set_line_width(1) 376 cr.stroke() 377 378 if self._orientation == Gtk.PageOrientation.LANDSCAPE: 379 cr.rotate(radians(-90)) 380 cr.translate(-paper_h, 0) 381 382 ##page_setup = self._context.get_page_setup() 383 ##cr.translate(page_setup.get_left_margin(Gtk.Unit.POINTS), 384 ##page_setup.get_top_margin(Gtk.Unit.POINTS)) 385 386 ##cr.set_source_surface(self.get_page(0)) 387 ##cr.paint() 388 389 # draw the content of the currently selected page 390 # Here we use dpi scaling instead of scaling the cairo context, 391 # because it gives better result. In the latter case the distance 392 # of glyphs was changing. 393 dpi = PRINTER_DPI * self._zoom 394 self._context.set_cairo_context(cr, dpi, dpi) 395 self._preview.render_page(self._current_page) 396 397 def on_swin_size_allocate(self, scrolledwindow, allocation): 398 if self._zoom_mode == ZOOM_FIT_WIDTH: 399 self.__set_zoom(self.__zoom_fit_width()) 400 401 if self._zoom_mode == ZOOM_BEST_FIT: 402 self.__set_zoom(self.__zoom_best_fit()) 403 404 def on_print_clicked(self, toolbutton): 405 pass 406 407 def on_first_clicked(self, toolbutton): 408 self.__set_page(0) 409 410 def on_prev_clicked(self, toolbutton): 411 self.__set_page(self._current_page - 1) 412 413 def on_next_clicked(self, toolbutton): 414 self.__set_page(self._current_page + 1) 415 416 def on_last_clicked(self, toolbutton): 417 self.__set_page(self._page_no - 1) 418 419 def on_entry_activate(self, entry): 420 try: 421 new_page = int(entry.get_text()) - 1 422 except ValueError: 423 new_page = self._current_page 424 425 if new_page < 0 or new_page >= self._page_no: 426 new_page = self._current_page 427 428 self.__set_page(new_page) 429 430 def on_zoom_fit_width_toggled(self, toggletoolbutton): 431 if toggletoolbutton.get_active(): 432 self._zoom_best_fit_button.set_active(False) 433 self._zoom_mode = ZOOM_FIT_WIDTH 434 self.__set_zoom(self.__zoom_fit_width()) 435 else: 436 self._zoom_mode = ZOOM_FREE 437 438 def on_zoom_best_fit_toggled(self, toggletoolbutton): 439 if toggletoolbutton.get_active(): 440 self._zoom_fit_width_button.set_active(False) 441 self._zoom_mode = ZOOM_BEST_FIT 442 self.__set_zoom(self.__zoom_best_fit()) 443 else: 444 self._zoom_mode = ZOOM_FREE 445 446 def on_zoom_in_clicked(self, toolbutton): 447 self._zoom_fit_width_button.set_active(False) 448 self._zoom_best_fit_button.set_active(False) 449 self._zoom_mode = ZOOM_FREE 450 self.__set_zoom(self.__zoom_in()) 451 452 def on_zoom_out_clicked(self, toolbutton): 453 self._zoom_fit_width_button.set_active(False) 454 self._zoom_best_fit_button.set_active(False) 455 self._zoom_mode = ZOOM_FREE 456 self.__set_zoom(self.__zoom_out()) 457 458 def on_window_delete_event(self, widget, event): 459 self.__end_preview() 460 return False 461 462 def on_quit_clicked(self, toolbutton): 463 self.__end_preview() 464 self._window.destroy() 465 466 # Public 467 468 def start(self): 469 # get paper/page dimensions 470 page_setup = self._context.get_page_setup() 471 self._paper_width = page_setup.get_paper_width(Gtk.Unit.POINTS) 472 self._paper_height = page_setup.get_paper_height(Gtk.Unit.POINTS) 473 self._page_width = page_setup.get_page_width(Gtk.Unit.POINTS) 474 self._page_height = page_setup.get_page_height(Gtk.Unit.POINTS) 475 self._orientation = page_setup.get_orientation() 476 477 # get the total number of pages 478 ##self._page_numbers = [0,] 479 ##self._page_surfaces = {} 480 self._page_no = self._operation.get_property('n_pages') 481 self._pages_label.set_text(_('of %d') % self._page_no) 482 483 # set zoom level and initial page number 484 self._zoom_mode = ZOOM_FREE 485 self.__set_zoom(1.0) 486 self.__set_page(0) 487 488 # let's the show begin... 489 self._window.show() 490 491#------------------------------------------------------------------------ 492# 493# GtkPrint class 494# 495#------------------------------------------------------------------------ 496class GtkPrint(libcairodoc.CairoDoc): 497 """Print document via GtkPrint* interface. 498 499 Requires Gtk+ 2.10. 500 501 """ 502 def run(self): 503 """Run the Gtk Print operation. 504 """ 505 global PRINT_SETTINGS 506 507 # get a page setup from the paper style we have 508 page_setup = paperstyle_to_pagesetup(self.paper) 509 510 # set up a print operation 511 operation = Gtk.PrintOperation() 512 operation.set_default_page_setup(page_setup) 513 operation.connect("begin_print", self.on_begin_print) 514 operation.connect("draw_page", self.on_draw_page) 515 operation.connect("paginate", self.on_paginate) 516 operation.connect("preview", self.on_preview) 517 518 # set print settings if it was stored previously 519 if PRINT_SETTINGS is not None: 520 operation.set_print_settings(PRINT_SETTINGS) 521 522 # run print dialog 523 while True: 524 self.preview = None 525 res = operation.run(Gtk.PrintOperationAction.PRINT_DIALOG, 526 self.uistate.window) 527 if self.preview is None: # cancel or print 528 break 529 # set up printing again; can't reuse PrintOperation? 530 operation = Gtk.PrintOperation() 531 operation.set_default_page_setup(page_setup) 532 operation.connect("begin_print", self.on_begin_print) 533 operation.connect("draw_page", self.on_draw_page) 534 operation.connect("paginate", self.on_paginate) 535 operation.connect("preview", self.on_preview) 536 # set print settings if it was stored previously 537 if PRINT_SETTINGS is not None: 538 operation.set_print_settings(PRINT_SETTINGS) 539 540 # store print settings if printing was successful 541 if res == Gtk.PrintOperationResult.APPLY: 542 PRINT_SETTINGS = operation.get_print_settings() 543 544 def on_begin_print(self, operation, context): 545 """Setup environment for printing. 546 """ 547 # get data from context here only once to save time on pagination 548 self.page_width = round(context.get_width()) 549 self.page_height = round(context.get_height()) 550 self.dpi_x = context.get_dpi_x() 551 self.dpi_y = context.get_dpi_y() 552 553 def on_paginate(self, operation, context): 554 """Paginate the whole document in chunks. 555 """ 556 layout = context.create_pango_layout() 557 558 finished = self.paginate(layout, 559 self.page_width, 560 self.page_height, 561 self.dpi_x, 562 self.dpi_y) 563 # update page number 564 operation.set_n_pages(len(self._pages)) 565 566 # start preview if needed 567 if finished and self.preview: 568 self.preview.start() 569 570 return finished 571 572 def on_draw_page(self, operation, context, page_nr): 573 """Draw the requested page. 574 """ 575 cr = context.get_cairo_context() 576 layout = context.create_pango_layout() 577 width = round(context.get_width()) 578 height = round(context.get_height()) 579 dpi_x = context.get_dpi_x() 580 dpi_y = context.get_dpi_y() 581 582 self.draw_page(page_nr, cr, layout, width, height, dpi_x, dpi_y) 583 584 def on_preview(self, operation, preview, context, parent): 585 """Implement custom print preview functionality. 586 """ 587 ##if constfunc.win()': 588 ##return False 589 590 self.preview = PrintPreview(operation, preview, context, parent) 591 592 # give a dummy cairo context to Gtk.PrintContext, 593 # PrintPreview will update it with the real one 594 try: 595 width = int(round(context.get_width())) 596 except ValueError: 597 width = 0 598 try: 599 height = int(round(context.get_height())) 600 except ValueError: 601 height = 0 602 surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) 603 cr = cairo.Context(surface) 604 context.set_cairo_context(cr, PRINTER_DPI, PRINTER_DPI) 605 606 return True 607