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