OLD | NEW |
1 // Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 #include "chrome/browser/printing/print_view_manager.h" | 5 #include "chrome/browser/printing/print_view_manager.h" |
6 | 6 |
7 #include "chrome/browser/browser_process.h" | 7 #include "chrome/browser/browser_process.h" |
8 #include "chrome/browser/printing/print_job.h" | 8 #include "chrome/browser/printing/print_job.h" |
9 #include "chrome/browser/printing/print_job_manager.h" | 9 #include "chrome/browser/printing/print_job_manager.h" |
10 #include "chrome/browser/printing/printed_document.h" | 10 #include "chrome/browser/printing/printed_document.h" |
11 #include "chrome/browser/printing/printer_query.h" | 11 #include "chrome/browser/printing/printer_query.h" |
12 #include "chrome/browser/renderer_host/render_view_host.h" | 12 #include "chrome/browser/renderer_host/render_view_host.h" |
13 #include "chrome/browser/tab_contents/navigation_entry.h" | 13 #include "chrome/browser/tab_contents/navigation_entry.h" |
14 #include "chrome/browser/tab_contents/web_contents.h" | 14 #include "chrome/browser/tab_contents/web_contents.h" |
15 #include "chrome/common/gfx/emf.h" | 15 #include "chrome/common/gfx/emf.h" |
16 #include "chrome/common/l10n_util.h" | 16 #include "chrome/common/l10n_util.h" |
17 #include "chrome/common/notification_service.h" | 17 #include "chrome/common/notification_service.h" |
18 | 18 |
19 #include "generated_resources.h" | 19 #include "generated_resources.h" |
20 | 20 |
21 using base::TimeDelta; | 21 using base::TimeDelta; |
22 | 22 |
23 namespace printing { | 23 namespace printing { |
24 | 24 |
25 PrintViewManager::PrintViewManager(WebContents& owner) | 25 PrintViewManager::PrintViewManager(WebContents& owner) |
26 : owner_(owner), | 26 : owner_(owner), |
27 waiting_to_print_(false), | 27 waiting_to_print_(false), |
28 inside_inner_message_loop_(false), | 28 inside_inner_message_loop_(false) { |
29 waiting_to_show_print_dialog_(false) { | |
30 memset(&print_params_, 0, sizeof(print_params_)); | 29 memset(&print_params_, 0, sizeof(print_params_)); |
31 } | 30 } |
32 | 31 |
33 PrintViewManager::~PrintViewManager() { | 32 PrintViewManager::~PrintViewManager() { |
34 } | 33 } |
35 | 34 |
36 void PrintViewManager::Destroy() { | 35 void PrintViewManager::Destroy() { |
37 DisconnectFromCurrentPrintJob(); | 36 DisconnectFromCurrentPrintJob(); |
38 } | 37 } |
39 | 38 |
40 void PrintViewManager::Stop() { | 39 void PrintViewManager::Stop() { |
41 // Cancel the current job, wait for the worker to finish. | 40 // Cancel the current job, wait for the worker to finish. |
42 TerminatePrintJob(true); | 41 TerminatePrintJob(true); |
43 } | 42 } |
44 | 43 |
45 void PrintViewManager::ShowPrintDialog() { | |
46 if (!CreateNewPrintJob(NULL)) | |
47 return; | |
48 | |
49 // Retrieve default settings. PrintJob will send back a | |
50 // NOTIFY_PRINT_JOB_EVENT with either INIT_DONE, INIT_CANCELED or FAILED. | |
51 // On failure, simply back off. Otherwise, request the number of pages to | |
52 // the renderer. Wait for its response (DidGetPrintedPagesCount), which will | |
53 // give the value used to initialize the Print... dialog. PrintJob will send | |
54 // back (again) a NOTIFY_PRINT_JOB_EVENT with either INIT_DONE, INIT_CANCELED | |
55 // or FAILED. The result is to call PrintNowInternal or to back off. | |
56 waiting_to_show_print_dialog_ = true; | |
57 print_job_->GetSettings(PrintJob::DEFAULTS, NULL); | |
58 } | |
59 | |
60 bool PrintViewManager::PrintNow() { | |
61 if (!CreateNewPrintJob(NULL)) | |
62 return false; | |
63 | |
64 // Retrieve default settings. PrintJob will send back a NOTIFY_PRINT_JOB_EVENT | |
65 // with either DEFAULT_INIT_DONE or FAILED. On failure, simply back off. | |
66 // Otherwise, call PrintNowInternal() again to start the print job. | |
67 waiting_to_print_ = true; | |
68 print_job_->GetSettings(PrintJob::DEFAULTS, NULL); | |
69 return true; | |
70 } | |
71 | |
72 bool PrintViewManager::OnRendererGone(RenderViewHost* render_view_host) { | 44 bool PrintViewManager::OnRendererGone(RenderViewHost* render_view_host) { |
73 if (!print_job_.get()) | 45 if (!print_job_.get()) |
74 return true; | 46 return true; |
75 | 47 |
76 if (render_view_host != owner_.render_view_host()) | 48 if (render_view_host != owner_.render_view_host()) |
77 return false; | 49 return false; |
78 | 50 |
79 scoped_refptr<PrintedDocument> document(print_job_->document()); | 51 scoped_refptr<PrintedDocument> document(print_job_->document()); |
80 if (document) { | 52 if (document) { |
81 // If IsComplete() returns false, the document isn't completely renderered. | 53 // If IsComplete() returns false, the document isn't completely renderered. |
(...skipping 12 matching lines...) Expand all Loading... |
94 PrintedDocument* document = print_job_->document(); | 66 PrintedDocument* document = print_job_->document(); |
95 if (!document || cookie != document->cookie()) { | 67 if (!document || cookie != document->cookie()) { |
96 // Out of sync. It may happens since we are completely asynchronous. Old | 68 // Out of sync. It may happens since we are completely asynchronous. Old |
97 // spurious message can happen if one of the processes is overloaded. | 69 // spurious message can happen if one of the processes is overloaded. |
98 return; | 70 return; |
99 } | 71 } |
100 | 72 |
101 // Time to inform our print job. Make sure it is for the right document. | 73 // Time to inform our print job. Make sure it is for the right document. |
102 if (!document->page_count()) { | 74 if (!document->page_count()) { |
103 document->set_page_count(number_pages); | 75 document->set_page_count(number_pages); |
104 if (waiting_to_show_print_dialog_) { | |
105 // Ask for user settings. There's a timing issue since we may not have | |
106 // received the INIT_DONE notification yet. If so, the dialog will be | |
107 // shown in Observe() since the page count arrived before the settings. | |
108 print_job_->GetSettings(PrintJob::ASK_USER, | |
109 ::GetParent(owner_.GetContainerHWND())); | |
110 waiting_to_show_print_dialog_ = false; | |
111 } | |
112 } | 76 } |
113 } | 77 } |
114 | 78 |
115 void PrintViewManager::DidPrintPage( | 79 void PrintViewManager::DidPrintPage( |
116 const ViewHostMsg_DidPrintPage_Params& params) { | 80 const ViewHostMsg_DidPrintPage_Params& params) { |
117 DCHECK(!inside_inner_message_loop_); | |
118 if (!OpportunisticallyCreatePrintJob(params.document_cookie)) | 81 if (!OpportunisticallyCreatePrintJob(params.document_cookie)) |
119 return; | 82 return; |
120 | 83 |
121 PrintedDocument* document = print_job_->document(); | 84 PrintedDocument* document = print_job_->document(); |
122 if (!document || params.document_cookie != document->cookie()) { | 85 if (!document || params.document_cookie != document->cookie()) { |
123 // Out of sync. It may happens since we are completely asynchronous. Old | 86 // Out of sync. It may happens since we are completely asynchronous. Old |
124 // spurious message can happen if one of the processes is overloaded. | 87 // spurious message can happen if one of the processes is overloaded. |
125 return; | 88 return; |
126 } | 89 } |
127 | 90 |
(...skipping 20 matching lines...) Expand all Loading... |
148 delete emf; | 111 delete emf; |
149 owner_.Stop(); | 112 owner_.Stop(); |
150 return; | 113 return; |
151 } | 114 } |
152 | 115 |
153 // Update the rendered document. It will send notifications to the listener. | 116 // Update the rendered document. It will send notifications to the listener. |
154 document->SetPage(params.page_number, emf, params.actual_shrink); | 117 document->SetPage(params.page_number, emf, params.actual_shrink); |
155 ShouldQuitFromInnerMessageLoop(); | 118 ShouldQuitFromInnerMessageLoop(); |
156 } | 119 } |
157 | 120 |
158 void PrintViewManager::RenderOnePrintedPage(PrintedDocument* document, | |
159 int page_number) { | |
160 // Currently a no-op. Rationale: printing is now completely synchronous and is | |
161 // handled by PrintAllPages. The reason is that PrintPreview is not used | |
162 // anymore and to make sure to not corrupt the screen, the whole generation is | |
163 // done synchronously. To make this work completely asynchronously, a | |
164 // duplicate copy of RenderView must be made to have an "innert" web page. | |
165 // Once this object is created, we'll have all the leasure to do whatever we | |
166 // want. | |
167 } | |
168 | |
169 std::wstring PrintViewManager::RenderSourceName() { | 121 std::wstring PrintViewManager::RenderSourceName() { |
170 std::wstring name(owner_.GetTitle()); | 122 std::wstring name(owner_.GetTitle()); |
171 if (name.empty()) | 123 if (name.empty()) |
172 name = l10n_util::GetString(IDS_DEFAULT_PRINT_DOCUMENT_TITLE); | 124 name = l10n_util::GetString(IDS_DEFAULT_PRINT_DOCUMENT_TITLE); |
173 return name; | 125 return name; |
174 } | 126 } |
175 | 127 |
176 GURL PrintViewManager::RenderSourceUrl() { | 128 GURL PrintViewManager::RenderSourceUrl() { |
177 NavigationEntry* entry = owner_.controller()->GetActiveEntry(); | 129 NavigationEntry* entry = owner_.controller()->GetActiveEntry(); |
178 if (entry) | 130 if (entry) |
(...skipping 21 matching lines...) Expand all Loading... |
200 const JobEventDetails& event_details) { | 152 const JobEventDetails& event_details) { |
201 switch (event_details.type()) { | 153 switch (event_details.type()) { |
202 case JobEventDetails::FAILED: { | 154 case JobEventDetails::FAILED: { |
203 // TODO(maruel): bug 1123882 Show some kind of notification. | 155 // TODO(maruel): bug 1123882 Show some kind of notification. |
204 TerminatePrintJob(true); | 156 TerminatePrintJob(true); |
205 break; | 157 break; |
206 } | 158 } |
207 case JobEventDetails::USER_INIT_DONE: | 159 case JobEventDetails::USER_INIT_DONE: |
208 case JobEventDetails::DEFAULT_INIT_DONE: | 160 case JobEventDetails::DEFAULT_INIT_DONE: |
209 case JobEventDetails::USER_INIT_CANCELED: { | 161 case JobEventDetails::USER_INIT_CANCELED: { |
210 OnNotifyPrintJobInitEvent(event_details); | 162 NOTREACHED(); |
211 break; | 163 break; |
212 } | 164 } |
213 case JobEventDetails::ALL_PAGES_REQUESTED: { | 165 case JobEventDetails::ALL_PAGES_REQUESTED: { |
214 ShouldQuitFromInnerMessageLoop(); | 166 ShouldQuitFromInnerMessageLoop(); |
215 break; | 167 break; |
216 } | 168 } |
217 case JobEventDetails::NEW_DOC: | 169 case JobEventDetails::NEW_DOC: |
218 case JobEventDetails::NEW_PAGE: | 170 case JobEventDetails::NEW_PAGE: |
219 case JobEventDetails::PAGE_DONE: { | 171 case JobEventDetails::PAGE_DONE: { |
220 // Don't care about the actual printing process. | 172 // Don't care about the actual printing process. |
(...skipping 10 matching lines...) Expand all Loading... |
231 ReleasePrintJob(); | 183 ReleasePrintJob(); |
232 break; | 184 break; |
233 } | 185 } |
234 default: { | 186 default: { |
235 NOTREACHED(); | 187 NOTREACHED(); |
236 break; | 188 break; |
237 } | 189 } |
238 } | 190 } |
239 } | 191 } |
240 | 192 |
241 void PrintViewManager::OnNotifyPrintJobInitEvent( | |
242 const JobEventDetails& event_details) { | |
243 ViewMsg_Print_Params old_print_params(print_params_); | |
244 | |
245 // Backup the print settings relevant to the renderer. | |
246 DCHECK_EQ(print_job_->document(), event_details.document()); | |
247 event_details.document()->settings().RenderParams(&print_params_); | |
248 print_params_.document_cookie = event_details.document()->cookie(); | |
249 DCHECK_GT(print_params_.document_cookie, 0); | |
250 | |
251 // If settings changed | |
252 DCHECK(owner_.render_view_host()); | |
253 // Equals() doesn't compare the cookie value. | |
254 if (owner_.render_view_host() && | |
255 owner_.render_view_host()->IsRenderViewLive() && | |
256 (!old_print_params.Equals(print_params_) || | |
257 !event_details.document()->page_count())) { | |
258 // TODO(maruel): Will never happen, this code is about to be deleted. | |
259 NOTREACHED(); | |
260 } | |
261 | |
262 // Continue even if owner_.render_view_host() is dead because we may already | |
263 // have buffered all the necessary pages. | |
264 switch (event_details.type()) { | |
265 case JobEventDetails::USER_INIT_DONE: { | |
266 // The user clicked the "Print" button in the Print... dialog. | |
267 // Time to print. | |
268 DCHECK_EQ(waiting_to_print_, false); | |
269 DCHECK_EQ(waiting_to_show_print_dialog_, false); | |
270 waiting_to_print_ = true; | |
271 PrintNowInternal(); | |
272 break; | |
273 } | |
274 case JobEventDetails::USER_INIT_CANCELED: { | |
275 DCHECK(!waiting_to_show_print_dialog_); | |
276 // The print dialog box has been dismissed (Cancel button or the X). | |
277 TerminatePrintJob(false); | |
278 break; | |
279 } | |
280 case JobEventDetails::DEFAULT_INIT_DONE: { | |
281 // Init(false) returned. | |
282 if (waiting_to_print_) { | |
283 // PrintNow() is pending. | |
284 DCHECK_EQ(waiting_to_show_print_dialog_, false); | |
285 PrintNowInternal(); | |
286 } else if (waiting_to_show_print_dialog_ && | |
287 event_details.document()->page_count()) { | |
288 // Time to ask the user for the print settings. | |
289 print_job_->GetSettings(PrintJob::ASK_USER, | |
290 ::GetParent(owner_.GetContainerHWND())); | |
291 waiting_to_show_print_dialog_ = false; | |
292 } else { | |
293 // event_details.document()->page_count() is false. This simply means | |
294 // that the renderer was slower to calculate the number of pages than | |
295 // the print_job_ to initialize the default settings. If so, the dialog | |
296 // will be shown in DidGetPrintedPagesCount() since the settings arrived | |
297 // before the page count. | |
298 DCHECK_EQ(waiting_to_show_print_dialog_, true); | |
299 } | |
300 break; | |
301 } | |
302 default: { | |
303 NOTREACHED(); | |
304 break; | |
305 } | |
306 } | |
307 } | |
308 | |
309 bool PrintViewManager::RenderAllMissingPagesNow() { | 193 bool PrintViewManager::RenderAllMissingPagesNow() { |
310 if (waiting_to_show_print_dialog_) { | |
311 // TODO(maruel): http://b/1186708 This happens in one of these case: | |
312 // - javascript:window.print();window.close(); which closes the window very | |
313 // fast. | |
314 // - The worker thread is hung, like the network printer failed, the network | |
315 // print server failed or the network cable is disconnected. | |
316 // In the first case we want to wait, but not on the second case. | |
317 | |
318 if (!RunInnerMessageLoop()) { | |
319 // This function is always called from DisconnectFromCurrentPrintJob() | |
320 // so we know that the job will be stopped/canceled in any case. | |
321 return false; | |
322 } | |
323 } | |
324 | |
325 if (!print_job_.get() || !print_job_->is_job_pending()) { | 194 if (!print_job_.get() || !print_job_->is_job_pending()) { |
326 DCHECK_EQ(waiting_to_print_, false); | 195 DCHECK_EQ(waiting_to_print_, false); |
327 return false; | 196 return false; |
328 } | 197 } |
329 | 198 |
330 // We can't print if there is no renderer. | 199 // We can't print if there is no renderer. |
331 if (!owner_.render_view_host() || | 200 if (!owner_.render_view_host() || |
332 !owner_.render_view_host()->IsRenderViewLive()) { | 201 !owner_.render_view_host()->IsRenderViewLive()) { |
333 waiting_to_print_ = false; | 202 waiting_to_print_ = false; |
334 return false; | 203 return false; |
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
368 // We are in a message loop created by RenderAllMissingPagesNow. Quit from | 237 // We are in a message loop created by RenderAllMissingPagesNow. Quit from |
369 // it. | 238 // it. |
370 MessageLoop::current()->Quit(); | 239 MessageLoop::current()->Quit(); |
371 inside_inner_message_loop_ = false; | 240 inside_inner_message_loop_ = false; |
372 waiting_to_print_ = false; | 241 waiting_to_print_ = false; |
373 } | 242 } |
374 } | 243 } |
375 | 244 |
376 bool PrintViewManager::CreateNewPrintJob(PrintJobWorkerOwner* job) { | 245 bool PrintViewManager::CreateNewPrintJob(PrintJobWorkerOwner* job) { |
377 DCHECK(!inside_inner_message_loop_); | 246 DCHECK(!inside_inner_message_loop_); |
378 if (waiting_to_print_ || waiting_to_show_print_dialog_) { | 247 if (waiting_to_print_) { |
379 // We can't help; we are waiting for a print job initialization. The user is | 248 // We can't help; we are waiting for a print job initialization. The user is |
380 // button bashing. The only thing we could do is to batch up the requests. | 249 // button bashing. The only thing we could do is to batch up the requests. |
381 return false; | 250 return false; |
382 } | 251 } |
383 | 252 |
384 // Disconnect the current print_job_. | 253 // Disconnect the current print_job_. |
385 DisconnectFromCurrentPrintJob(); | 254 DisconnectFromCurrentPrintJob(); |
386 | 255 |
387 // We can't print if there is no renderer. | 256 // We can't print if there is no renderer. |
388 if (!owner_.render_view_host() || | 257 if (!owner_.render_view_host() || |
389 !owner_.render_view_host()->IsRenderViewLive()) { | 258 !owner_.render_view_host()->IsRenderViewLive()) { |
390 return false; | 259 return false; |
391 } | 260 } |
392 | 261 |
393 // Ask the renderer to generate the print preview, create the print preview | 262 // Ask the renderer to generate the print preview, create the print preview |
394 // view and switch to it, initialize the printer and show the print dialog. | 263 // view and switch to it, initialize the printer and show the print dialog. |
395 DCHECK(!print_job_.get()); | 264 DCHECK(!print_job_.get()); |
396 if (job) { | 265 DCHECK(job); |
397 print_job_ = new PrintJob(); | 266 if (!job) |
398 print_job_->Initialize(job, this); | 267 return false; |
399 } else { | 268 |
400 print_job_ = new PrintJob(this); | 269 print_job_ = new PrintJob(); |
401 } | 270 print_job_->Initialize(job, this); |
402 NotificationService::current()->AddObserver( | 271 NotificationService::current()->AddObserver( |
403 this, | 272 this, |
404 NotificationType::PRINT_JOB_EVENT, | 273 NotificationType::PRINT_JOB_EVENT, |
405 Source<PrintJob>(print_job_.get())); | 274 Source<PrintJob>(print_job_.get())); |
406 return true; | 275 return true; |
407 } | 276 } |
408 | 277 |
409 void PrintViewManager::DisconnectFromCurrentPrintJob() { | 278 void PrintViewManager::DisconnectFromCurrentPrintJob() { |
410 // Make sure all the necessary rendered page are done. Don't bother with the | 279 // Make sure all the necessary rendered page are done. Don't bother with the |
411 // return value. | 280 // return value. |
(...skipping 13 matching lines...) Expand all Loading... |
425 } | 294 } |
426 | 295 |
427 void PrintViewManager::TerminatePrintJob(bool cancel) { | 296 void PrintViewManager::TerminatePrintJob(bool cancel) { |
428 if (!print_job_.get()) | 297 if (!print_job_.get()) |
429 return; | 298 return; |
430 | 299 |
431 if (cancel) { | 300 if (cancel) { |
432 // We don't need the EMF data anymore because the printing is canceled. | 301 // We don't need the EMF data anymore because the printing is canceled. |
433 print_job_->Cancel(); | 302 print_job_->Cancel(); |
434 waiting_to_print_ = false; | 303 waiting_to_print_ = false; |
435 waiting_to_show_print_dialog_ = false; | |
436 inside_inner_message_loop_ = false; | 304 inside_inner_message_loop_ = false; |
437 } else { | 305 } else { |
438 DCHECK(!inside_inner_message_loop_); | 306 DCHECK(!inside_inner_message_loop_); |
439 DCHECK(!waiting_to_show_print_dialog_); | |
440 DCHECK(!print_job_->document() || print_job_->document()->IsComplete() || | 307 DCHECK(!print_job_->document() || print_job_->document()->IsComplete() || |
441 !waiting_to_print_); | 308 !waiting_to_print_); |
442 | 309 |
443 // WebContents is either dying or navigating elsewhere. We need to render | 310 // WebContents is either dying or navigating elsewhere. We need to render |
444 // all the pages in an hurry if a print job is still pending. This does the | 311 // all the pages in an hurry if a print job is still pending. This does the |
445 // trick since it runs a blocking message loop: | 312 // trick since it runs a blocking message loop: |
446 print_job_->Stop(); | 313 print_job_->Stop(); |
447 } | 314 } |
448 ReleasePrintJob(); | 315 ReleasePrintJob(); |
449 } | 316 } |
(...skipping 13 matching lines...) Expand all Loading... |
463 memset(&print_params_, 0, sizeof(print_params_)); | 330 memset(&print_params_, 0, sizeof(print_params_)); |
464 } | 331 } |
465 | 332 |
466 void PrintViewManager::PrintNowInternal() { | 333 void PrintViewManager::PrintNowInternal() { |
467 DCHECK(waiting_to_print_); | 334 DCHECK(waiting_to_print_); |
468 | 335 |
469 // Settings are already loaded. Go ahead. This will set | 336 // Settings are already loaded. Go ahead. This will set |
470 // print_job_->is_job_pending() to true. | 337 // print_job_->is_job_pending() to true. |
471 print_job_->StartPrinting(); | 338 print_job_->StartPrinting(); |
472 | 339 |
473 if (!print_job_->document() || | 340 DCHECK(print_job_->document()); |
474 !print_job_->document()->IsComplete()) { | 341 DCHECK(print_job_->document()->IsComplete()); |
475 // TODO(maruel): Will never happen. This code is about to be deleted. | |
476 NOTREACHED(); | |
477 } | |
478 } | 342 } |
479 | 343 |
480 bool PrintViewManager::RunInnerMessageLoop() { | 344 bool PrintViewManager::RunInnerMessageLoop() { |
481 // This value may actually be too low: | 345 // This value may actually be too low: |
482 // | 346 // |
483 // - If we're looping because of printer settings initializaton, the premise | 347 // - If we're looping because of printer settings initializaton, the premise |
484 // here is that some poor users have their print server away on a VPN over | 348 // here is that some poor users have their print server away on a VPN over |
485 // dialup. In this situation, the simple fact of opening the printer can be | 349 // dialup. In this situation, the simple fact of opening the printer can be |
486 // dead slow. On the other side, we don't want to die infinitely for a real | 350 // dead slow. On the other side, we don't want to die infinitely for a real |
487 // network error. Give the printer 60 seconds to comply. | 351 // network error. Give the printer 60 seconds to comply. |
(...skipping 50 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
538 } | 402 } |
539 | 403 |
540 // Settings are already loaded. Go ahead. This will set | 404 // Settings are already loaded. Go ahead. This will set |
541 // print_job_->is_job_pending() to true. | 405 // print_job_->is_job_pending() to true. |
542 print_job_->StartPrinting(); | 406 print_job_->StartPrinting(); |
543 return true; | 407 return true; |
544 } | 408 } |
545 | 409 |
546 } // namespace printing | 410 } // namespace printing |
547 | 411 |
OLD | NEW |