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