]> git.vomp.tv Git - vompclient.git/blob - vvideomedia.cc
Fix black background for 16:9 live TV on 4:3 screen
[vompclient.git] / vvideomedia.cc
1 /*
2     Copyright 2004-2005 Chris Tallon
3
4     This file is part of VOMP.
5
6     VOMP is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10
11     VOMP is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with VOMP.  If not, see <https://www.gnu.org/licenses/>.
18 */
19
20 #include "vvideomedia.h"
21 #include "vmedialist.h"
22 #include "media.h"
23 #include "mediaplayer.h"
24
25 #include "command.h"
26 #include "osd.h"
27 #include "wsymbol.h"
28 #include "audio.h"
29 #include "video.h"
30 #include "playermedia.h"
31 #include "recording.h"
32 #include "vaudioselector.h"
33 #include "message.h"
34 #include "input.h"
35 #include "boxstack.h"
36 #include "vinfo.h"
37 #include "i18n.h"
38 #include "log.h"
39 #include "recinfo.h"
40 #include "messagequeue.h"
41
42 //use the picture channel
43 #define MEDIACHANNEL 1
44
45 //we misuse PLAYER_EVENTS for timer messages
46 //this should be larger then any player message
47 #define PLAYER_TIMER_BASE 100
48
49 VVideoMedia::VVideoMedia(Media* media, VMediaList *p)
50 {
51   lparent=p;
52   boxstack = BoxStack::getInstance();
53   video = Video::getInstance();
54   timers = Timers::getInstance();
55   vas = NULL;
56   vsummary = NULL;
57   lengthBytes=0;
58   myMedia = new Media(media);
59
60   player = new PlayerMedia(this);
61   player->run();
62
63   videoMode = video->getMode();
64
65   playing = false;
66
67
68   setSize(video->getScreenWidth(), video->getScreenHeight());
69   createBuffer();
70   transparent.set(0, 0, 0, 0);
71   setBackgroundColour(transparent);
72
73   barRegion.x = 0;
74   barRegion.y = video->getScreenHeight() - 58;   // FIXME, need to be - 1? and below?
75   barRegion.w = video->getScreenWidth();
76   barRegion.h = 58;
77
78   clocksRegion.x = barRegion.x + 140;
79   clocksRegion.y = barRegion.y + 12;
80   clocksRegion.w = 170;
81   clocksRegion.h = getFontHeight();
82
83
84   barBlue.set(0, 0, 150, 150);
85
86   barShowing = false;
87   barGenHold = false;
88   barScanHold = false;
89   barVasHold = false;
90
91   dowss = false;
92   char* optionWSS = VDR::getInstance()->configLoad("General", "WSS");
93   if (optionWSS)
94   {
95     if (strstr(optionWSS, "Yes")) dowss = true;
96     delete[] optionWSS;
97   }
98   Log::getInstance()->log("VVideoMedia", Log::DEBUG, "Do WSS: %u", dowss);
99
100   if (dowss)
101   {
102     wss.setFormat(video->getFormat());
103     wss.setWide(true);
104     add(&wss);
105
106     wssRegion.x = 0;
107     wssRegion.y = 0;
108     wssRegion.w = video->getScreenWidth();
109     wssRegion.h = 300;
110   }
111 }
112
113 VVideoMedia::~VVideoMedia()
114 {
115   Log::getInstance()->log("VVideoMedia", Log::DEBUG, "Entering  destructor");
116
117   if (vas)
118   {
119     boxstack->remove(vas);
120     vas = NULL;
121   }
122
123   if (vsummary) {
124     remove(vsummary);
125     delete vsummary;
126   }
127
128   if (playing) stopPlay();
129   video->setDefaultAspect();
130
131   timers->cancelTimer(this, 1);
132   timers->cancelTimer(this, 2);
133
134   delete myMedia;
135   Log::getInstance()->log("VVideoMedia", Log::DEBUG, "shutting down player");
136   player->shutdown();
137   delete player;
138   Log::getInstance()->log("VVideoMedia", Log::DEBUG, "deleted");
139 }
140
141 void VVideoMedia::go(bool resume)
142 {
143   ULONG startFrameNum=0;
144
145   Log::getInstance()->log("VVideoMedia", Log::DEBUG, "Starting stream: %s at frame: %lu", myMedia->getFileName(), startFrameNum);
146
147   lengthBytes = 0;
148
149   int rt=0;
150   const MediaURI *u=myMedia->getURI();
151   if (!u) {
152     Log::getInstance()->log("VVideoMedia", Log::ERR, "stream: %s has no URI", myMedia->getFileName());
153     rt=-1;
154   }
155   else {
156     rt=MediaPlayer::getInstance()->openMedium(MEDIACHANNEL,u,&lengthBytes,area.w,area.h);
157   }
158   if (rt==0)
159   {
160     //TODO: figure out len in frames
161     int seq=player->playNew(MEDIACHANNEL,lengthBytes,0);
162     int ok=player->waitForSequence(2,seq);
163     if (ok < 0) rt=-1; 
164     else {
165       playing = true;
166       doBar(0);
167     }
168   }
169   if (rt != 0)
170   {
171     stopPlay(); // clean up
172
173     Message* m = new Message();
174     m->message = Message::CLOSE_ME;
175     m->from = this;
176     m->to = boxstack;
177     MessageQueue::getInstance()->postMessage(m);
178
179     VInfo* vi = new VInfo();
180     vi->setSize(400, 150);
181     vi->createBuffer();
182     if (video->getFormat() == Video::PAL)
183       vi->setPosition(170, 200);
184     else
185       vi->setPosition(160, 150);
186     vi->setExitable();
187     vi->setBorderOn(1);
188     vi->setTitleBarOn(0);
189     vi->setOneLiner(tr("Error playing media"));
190     vi->draw();
191
192     m = new Message();
193     m->message = Message::ADD_VIEW;
194     m->to = boxstack;
195     m->data = reinterpret_cast<void*>(vi);
196     MessageQueue::getInstance()->postMessage(m);
197   }
198 }
199
200 int VVideoMedia::handleCommand(int command)
201 {
202   switch(command)
203   {
204     case Input::PLAY:
205     {
206       player->play();
207       doBar(0);
208       return 2;
209     }
210
211     case Input::BACK:
212     {
213       if (vsummary)
214       {
215         removeSummary();
216         return 2;
217       }
218     } // DROP THROUGH
219     case Input::STOP:
220     case Input::MENU:
221     {
222       if (playing) stopPlay();
223
224       return 4;
225     }
226     case Input::PAUSE:
227     {
228       player->pause();
229       doBar(0);
230       return 2;
231     }
232     case Input::SKIPFORWARD:
233     {
234       doBar(3);
235       player->skipForward(60);
236       return 2;
237     }
238     case Input::SKIPBACK:
239     {
240       doBar(4);
241       player->skipBackward(60);
242       return 2;
243     }
244     case Input::FORWARD:
245     {
246       player->fastForward();
247       doBar(0);
248       return 2;
249     }
250     case Input::REVERSE:
251     {
252       player->fastBackward();
253       doBar(0);
254       return 2;
255     }
256     case Input::RED:
257     {
258       if (vsummary) removeSummary();
259       else doSummary();
260       return 2;
261     }
262     case Input::GREEN:
263     {
264       doAudioSelector();
265       return 2;
266     }
267     case Input::YELLOW:
268     {
269       doBar(2);
270       player->skipBackward(10);
271       return 2;
272     }
273     case Input::BLUE:
274     {
275       doBar(1);
276       player->skipForward(10);
277       return 2;
278     }
279     case Input::STAR:
280     {
281       doBar(2);
282       player->skipBackward(10);
283       return 2;
284     }
285     case Input::HASH:
286     {
287       doBar(1);
288       player->skipForward(10);
289       return 2;
290     }
291     case Input::FULL:
292     case Input::TV:
293     {
294       toggleChopSides();
295       return 2;
296     }
297
298     case Input::OK:
299     {
300       if (vsummary)
301       {
302         removeSummary();
303         return 2;
304       }
305       
306       if (barShowing) removeBar();
307       else {
308         doBar(0);
309         barGenHold=true;
310       }
311       return 2;
312     }
313
314     case Input::ZERO:  player->jumpToPercent(0);  doBar(0);  return 2;
315     case Input::ONE:   player->jumpToPercent(10); doBar(0);  return 2;
316     case Input::TWO:   player->jumpToPercent(20); doBar(0);  return 2;
317     case Input::THREE: player->jumpToPercent(30); doBar(0);  return 2;
318     case Input::FOUR:  player->jumpToPercent(40); doBar(0);  return 2;
319     case Input::FIVE:  player->jumpToPercent(50); doBar(0);  return 2;
320     case Input::SIX:   player->jumpToPercent(60); doBar(0);  return 2;
321     case Input::SEVEN: player->jumpToPercent(70); doBar(0);  return 2;
322     case Input::EIGHT: player->jumpToPercent(80); doBar(0);  return 2;
323     case Input::NINE:  player->jumpToPercent(90); doBar(0);  return 2;
324
325
326   }
327
328   return 1;
329 }
330
331 void VVideoMedia::processMessage(Message* m)
332 {
333   Log::getInstance()->log("VVideoMedia", Log::DEBUG, "Message received");
334
335   if (m->message == Message::MOUSE_LBDOWN)
336   {
337     UINT x = (m->parameter>>16) - getScreenX();
338     UINT y = (m->parameter&0xFFFF) - getScreenY();
339
340     if (!barShowing)
341     {
342       BoxStack::getInstance()->handleCommand(Input::OK); //simulate rok press
343     }
344     else if (barRegion.x<=x && barRegion.y<=y && (barRegion.x+barRegion.w)>=x && (barRegion.y+barRegion.h)>=y)
345     {
346       int progBarXbase = barRegion.x + 300;
347       if (x>=barRegion.x + progBarXbase + 24
348           && x<=barRegion.x + progBarXbase + 4 + 302
349           && y>=barRegion.y + 12 - 2
350           && y<=barRegion.y + 12 - 2+28)
351       {
352         int cx=x-(barRegion.x + progBarXbase + 4);
353         double percent=((double)cx)/302.*100.;
354         player->jumpToPercent(percent);
355         doBar(3);
356         return;
357         //  int progressWidth = 302 * currentFrameNum / lengthFrames;
358         //  rectangle(barRegion.x + progBarXbase + 4, barRegion.y + 16, progressWidth, 16, DrawStyle::SELECTHIGHLIGHT);
359       }
360     }
361     else
362     {
363       BoxStack::getInstance()->handleCommand(Input::OK); //simulate rok press
364     }
365   }
366   else if (m->message == Message::PLAYER_EVENT)
367   {
368     switch(m->parameter)
369     {
370       case PlayerMedia::CONNECTION_LOST: // connection lost detected
371       {
372         // I can't handle this, send it to command
373         Message* m2 = new Message();
374         m2->to = Command::getInstance();
375         m2->message = Message::CONNECTION_LOST;
376         MessageQueue::getInstance()->postMessage(m2);
377         break;
378       }
379       case PlayerMedia::STREAM_END:
380       {
381         Message* m2 = new Message(); // Must be done after this thread finishes, and must break into master mutex
382         m2->to = BoxStack::getInstance();
383         m2->message = Message::CLOSE_ME;
384         MessageQueue::getInstance()->postMessage(m2);
385         break;
386       }
387       case PlayerMedia::STATUS_CHANGE:
388         doBar(0);
389         break;
390       case PlayerMedia::ASPECT43:
391       {
392         if (dowss)
393         {
394           Log::getInstance()->log("VVideoMedia", Log::DEBUG, "Received do WSS 43");
395           wss.setWide(false);
396           wss.draw();
397           boxstack->update(this, &wssRegion);
398         }
399         break;
400       }
401       case PlayerMedia::ASPECT169:
402       {
403         if (dowss)
404         {
405           Log::getInstance()->log("VVideoMedia", Log::DEBUG, "Received do WSS 169");
406           wss.setWide(true);
407           wss.draw();
408           boxstack->update(this, &wssRegion);
409         }
410         break;
411       }
412       case (PLAYER_TIMER_BASE+1) :
413         //timer1:
414         // Remove bar
415         removeBar();
416         break;
417       case (PLAYER_TIMER_BASE+2) :
418         //timer2:
419         // Update clock
420         if (!barShowing) break;
421         drawBarClocks();
422         BoxStack::getInstance()->update(this,&barRegion);
423         if (player->getLengthFrames() != 0)   timers->setTimerD(this, 2, 0, 200000000);
424         else   timers->setTimerD(this, 2, 1);
425         break;
426     }
427   }
428   else if (m->message == Message::AUDIO_CHANGE_CHANNEL)
429   {
430     Log::getInstance()->log("VVideoMedia", Log::DEBUG, "Received change audio channel to %i", m->parameter);
431     player->setAudioChannel(m->parameter);
432   }
433   else if (m->message == Message::CHILD_CLOSE)
434   {
435     if (m->from == vas)
436     {
437       vas = NULL;
438       barVasHold = false;
439       if (!barGenHold && !barScanHold && !barVasHold) removeBar();
440     }
441   }
442 }
443
444 void VVideoMedia::stopPlay()
445 {
446   Log::getInstance()->log("VVideoMedia", Log::DEBUG, "Pre stopPlay");
447
448   removeBar();
449
450   player->stop();
451
452   playing = false;
453   MediaPlayer::getInstance()->closeMediaChannel(MEDIACHANNEL);
454
455   Log::getInstance()->log("VVideoMedia", Log::DEBUG, "Post stopPlay");
456 }
457
458 void VVideoMedia::toggleChopSides()
459 {
460   if (video->getTVsize() == Video::ASPECT16X9) return; // Means nothing for 16:9 TVs
461
462   if (videoMode == Video::NORMAL)
463   {
464     videoMode = Video::LETTERBOX;
465     video->setMode(Video::LETTERBOX);
466   }
467   else
468   {
469     videoMode = Video::NORMAL;
470     video->setMode(Video::NORMAL);
471   }
472 }
473
474 void VVideoMedia::doAudioSelector()
475 {
476   bool* availableMpegAudioChannels = player->getDemuxerMpegAudioChannels();
477   bool* availableAc3AudioChannels = 0;
478   int currentAudioChannel = player->getCurrentAudioChannel();
479   if (Audio::getInstance()->supportsAc3())
480   {
481       availableAc3AudioChannels = player->getDemuxerAc3AudioChannels();
482   }
483
484
485   RecInfo ri;
486   ri.summary=new char[strlen(myMedia->getDisplayName())+1];
487   strcpy(ri.summary,myMedia->getDisplayName());
488   vas = new VAudioSelector(this, availableMpegAudioChannels, availableAc3AudioChannels, currentAudioChannel, NULL,NULL,0,0, &ri);
489  
490   vas->setBackgroundColour(barBlue);
491   vas->setPosition(0, barRegion.y - 120);
492
493 // pal 62, ntsc 57
494
495   barVasHold = true;
496   doBar(0);
497
498   vas->draw();
499   boxstack->add(vas);
500   boxstack->update(vas);
501 }
502
503 void VVideoMedia::doBar(int action)
504 {
505   Log::getInstance()->log("VVideoMedia",Log::DEBUG,"doBar %d",action);
506   barShowing = true;
507
508   rectangle(barRegion, barBlue);
509
510   /* Work out what to display - choices:
511
512   Playing  >
513   Paused   ||
514   FFwd     >>
515   FBwd     <<
516
517   Specials, informed by parameter
518
519   Skip forward 10s    >|
520   Skip backward 10s   |<
521   Skip forward 1m     >>|
522   Skip backward 1m    |<<
523
524   */
525
526   WSymbol w;
527   TEMPADD(&w);
528   w.nextSymbol = 0;
529   w.setPosition(barRegion.x + 66, barRegion.y + 16);
530
531   UCHAR playerState = 0;
532
533   if (action)
534   {
535     if (action == 1)       w.nextSymbol = WSymbol::SKIPFORWARD;
536     else if (action == 2)  w.nextSymbol = WSymbol::SKIPBACK;
537     else if (action == 3)  w.nextSymbol = WSymbol::SKIPFORWARD2;
538     else if (action == 4)  w.nextSymbol = WSymbol::SKIPBACK2;
539   }
540   else
541   {
542     playerState = player->getState();
543     if (playerState == PlayerMedia::S_PLAY)      w.nextSymbol = WSymbol::PLAY;
544     else if (playerState == PlayerMedia::S_FF)    w.nextSymbol = WSymbol::FFWD;
545     else if (playerState == PlayerMedia::S_BACK)    w.nextSymbol = WSymbol::FBWD;
546     else if (playerState == PlayerMedia::S_SEEK)    w.nextSymbol = WSymbol::RIGHTARROW;
547     else if (playerState == PlayerMedia::S_STOP)    w.nextSymbol = WSymbol::PAUSE;
548     else                                       w.nextSymbol = WSymbol::PAUSE;
549   }
550
551   w.draw();
552
553   if ((playerState == PlayerMedia::S_FF) || (playerState == PlayerMedia::S_BACK))
554   {
555     // draw blips to show how fast the scan is
556     UCHAR scanrate = 2;//player->getIScanRate();
557     if (scanrate >= 2)
558     {
559       char text[5];
560       SNPRINTF(text, 5, "%ux", scanrate);
561       drawText(text, barRegion.x + 102, barRegion.y + 12, DrawStyle::LIGHTTEXT);
562     }
563   }
564
565   drawBarClocks();
566   boxstack->update(this, &barRegion);
567
568   timers->cancelTimer(this, 1);
569
570
571   if ((playerState == PlayerMedia::S_FF) || (playerState == PlayerMedia::S_BACK)) barScanHold = true;
572   else barScanHold = false;
573
574   if (!barGenHold && !barScanHold && !barVasHold) timers->setTimerD(this, 1, 4);
575
576   if (player->getLengthFrames() != 0) timers->setTimerD(this, 2, 0, 200000000);
577   else timers->setTimerD(this, 2, 1);
578 }
579
580 void VVideoMedia::timercall(int clientReference)
581 {
582   Message *m=new Message();
583   m->message=Message::PLAYER_EVENT;
584   m->to=this;
585   m->from=this;
586   m->parameter=PLAYER_TIMER_BASE+clientReference;
587   MessageQueue::getInstance()->postMessage(m);
588 }
589
590 void VVideoMedia::drawBarClocks()
591 {
592   if (barScanHold)
593   {
594     UCHAR playerState = player->getState();
595     // sticky bar is set if we are in ffwd/fbwd mode
596     // if player has gone to S_PLAY then kill stickyBar, and run doBar(0) which
597     // will repaint all the bar (it will call this function again, but
598     // this section won't run because stickyBarF will then == false)
599
600     if ((playerState != PlayerMedia::S_FF) && (playerState != PlayerMedia::S_BACK))
601     {
602       barScanHold = false;
603       doBar(0);
604       return; 
605     }
606   }
607
608   Log* logger = Log::getInstance();
609   logger->log("VVideoMedia", Log::DEBUG, "Draw bar clocks");
610
611   // Draw RTC
612   // Blank the area first
613   rectangle(barRegion.x + 624, barRegion.y + 12, 60, 30, barBlue);
614   char timeString[20];
615   time_t t;
616   time(&t);
617   struct tm tms;
618   LOCALTIME_R(&t, &tms);
619
620   strftime(timeString, 19, "%H:%M", &tms);
621   drawText(timeString, barRegion.x + 624, barRegion.y + 12, DrawStyle::LIGHTTEXT);
622
623   ULLONG lenPTS=player->getLenPTS();
624   // Draw clocks
625
626   rectangle(clocksRegion, barBlue);
627
628   ULLONG currentPTS = player->getCurrentPTS();
629
630   hmsf currentFrameHMSF = ptsToHMS(currentPTS);
631   hmsf lengthHMSF = ptsToHMS(lenPTS);
632
633   char buffer[100];
634   if (currentPTS > lenPTS && lenPTS != 0)
635   {
636     strcpy(buffer, "-:--:-- / -:--:--");
637   }
638   else
639   {
640     SNPRINTF(buffer, 99, "%01i:%02i:%02i / %01i:%02i:%02i", currentFrameHMSF.hours, currentFrameHMSF.minutes, currentFrameHMSF.seconds, lengthHMSF.hours, lengthHMSF.minutes, lengthHMSF.seconds);
641   }
642     logger->log("VVideoMedia", Log::DEBUG, "cur %llu,len %llu, txt %s",currentPTS,lenPTS,buffer);
643
644   drawText(buffer, clocksRegion.x, clocksRegion.y, DrawStyle::LIGHTTEXT);
645
646
647
648
649
650
651
652   // Draw progress bar
653   int progBarXbase = barRegion.x + 300;
654
655   if (lenPTS == 0) return;
656   rectangle(barRegion.x + progBarXbase, barRegion.y + 12, 310, 24, DrawStyle::LIGHTTEXT);
657   rectangle(barRegion.x + progBarXbase + 2, barRegion.y + 14, 306, 20, barBlue);
658
659   if (currentPTS > lenPTS) return;
660
661   // Draw yellow portion
662   int progressWidth = 302 * currentPTS / lenPTS;
663   rectangle(barRegion.x + progBarXbase + 4, barRegion.y + 16, progressWidth, 16, DrawStyle::SELECTHIGHLIGHT);
664
665 }
666
667 void VVideoMedia::removeBar()
668 {
669   if (!barShowing) return;
670   timers->cancelTimer(this, 2);
671   barShowing = false;
672   barGenHold = false;
673   barScanHold = false;
674   barVasHold = false;
675   rectangle(barRegion, transparent);
676   BoxStack::getInstance()->update(this, &barRegion);
677 }
678
679 void VVideoMedia::doSummary()
680 {
681   vsummary = new VInfo();
682   vsummary->setTitleText(myMedia->getDisplayName());
683   vsummary->setBorderOn(1);
684   vsummary->setExitable();
685   const MediaURI *u=myMedia->getURI();
686   int stlen=0;
687   if (u) {
688     stlen+=strlen(myMedia->getFileName());
689     stlen+=strlen(tr("FileName"))+10;
690   }
691   stlen+=strlen(tr("Size"))+50;
692   stlen+=strlen(tr("Directory"))+500;
693   stlen+=strlen(tr("Time"))+50;
694   char *pinfo=player->getInfo();
695   stlen+=strlen(pinfo)+10;
696   char *buf=new char [stlen];
697   char *tsbuf=new char [stlen];
698   char *tsbuf2=new char [stlen];
699   char *tbuf=new char[Media::TIMEBUFLEN];
700   SNPRINTF(buf,stlen,"%s\n" 
701                    "%s: %llu Bytes\n"
702                    "%s\n"
703                    "%s: %s\n"
704                    "%s",
705       shortendedText(tr("FileName"),": ",myMedia->getFileName(),tsbuf,vsummary->getWidth()),
706       tr("Size"),
707       lengthBytes,
708       shortendedText(tr("Directory"),": ",lparent->getDirname(MEDIA_TYPE_VIDEO),tsbuf2,vsummary->getWidth()),
709       tr("Time"),
710       myMedia->getTimeString(tbuf),
711       pinfo
712       );
713   //TODO more info
714   if (u) {
715       Log::getInstance()->log("VVideoMedia",Log::DEBUG,"info %s",buf);
716       vsummary->setMainText(buf);
717   }
718   else vsummary->setMainText(tr("Info unavailable"));
719   delete pinfo;
720   if (Video::getInstance()->getFormat() == Video::PAL)
721   {
722     vsummary->setPosition(70, 100);
723   }
724   else
725   {
726     vsummary->setPosition(40, 70);
727   }
728   vsummary->setSize(580, 350);
729   add(vsummary);
730   vsummary->draw();
731
732   BoxStack::getInstance()->update(this);
733    delete [] buf;
734   delete []  tsbuf;
735   delete [] tsbuf2;
736   delete []  tbuf;
737 }
738
739 void VVideoMedia::removeSummary()
740 {
741   if (vsummary)
742   {
743     remove(vsummary);
744     delete vsummary;
745     vsummary = NULL;
746     draw();
747     BoxStack::getInstance()->update(this);
748   }
749 }
750
751
752 hmsf VVideoMedia::ptsToHMS(ULLONG pts) {
753   ULLONG secs=pts/90000;
754   hmsf rt;
755   rt.frames=0;
756   rt.seconds=secs%60;
757   secs=secs/60;
758   rt.minutes=secs%60;
759   secs=secs/60;
760   rt.hours=secs;
761   return rt;
762 }
763   
764 char * VVideoMedia::shortendedText(const char * title, const char * title2,const char * intext,char *buffer,UINT width) {
765   if (! intext) {
766     intext="";
767   }
768   UINT twidth=0;
769   for (const char *p=title;*p!=0;p++) twidth+=(UINT)charWidth(*p);
770   for (const char *p=title2;*p!=0;p++) twidth+=(UINT)charWidth(*p);
771   const char *prfx="...";
772   UINT prfwidth=(UINT)(3*charWidth('.'));
773   const char *istart=intext+strlen(intext);
774   UINT iwidth=0;
775   while (twidth+iwidth+prfwidth < width-2*paraMargin && istart> intext) {
776     istart--;
777     iwidth+=(UINT)charWidth(*istart);
778   }
779   if (twidth+iwidth+prfwidth >= width-2*paraMargin && istart < intext+strlen(intext)) istart++;
780   if (istart == intext) prfx="";
781   sprintf(buffer,"%s%s%s%s",title,title2,prfx,intext);
782   return buffer;
783 }
784
785