1 //////////////////////////////////////////////////////////////////////
2 //
3 // BeeBEEP Copyright (C) 2010-2021 Marco Mastroddi
4 //
5 // BeeBEEP is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published
7 // by the Free Software Foundation, either version 3 of the License,
8 // or (at your option) any later version.
9 //
10 // BeeBEEP is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with BeeBEEP. If not, see <http://www.gnu.org/licenses/>.
17 //
18 // Author: Marco Mastroddi <marco.mastroddi(AT)gmail.com>
19 //
20 // $Id: BeeUtils.cpp 1469 2021-01-05 15:03:48Z mastroddi $
21 //
22 //////////////////////////////////////////////////////////////////////
23 
24 #include "Avatar.h"
25 #include "BeeUtils.h"
26 #include "ChatMessage.h"
27 #include "IconManager.h"
28 #include "MessageManager.h"
29 #include "PluginManager.h"
30 #include "Protocol.h"
31 #include "User.h"
32 #include "Settings.h"
33 #if QT_VERSION < 0x050000
34   #ifdef Q_OS_WIN
35     #include <Windows.h>
36   #endif
37 #endif
38 #ifndef Q_OS_WIN
39   #include <utime.h>
40   #include <errno.h>
41 #endif
42 
43 
userStatusIconFileName(int user_status)44 QString Bee::userStatusIconFileName( int user_status )
45 {
46   switch( user_status )
47   {
48   case User::Offline:
49     return IconManager::instance().iconPath( "user-offline.png" );
50   case User::Away:
51     return IconManager::instance().iconPath( "user-away.png" );
52   case User::Busy:
53     return IconManager::instance().iconPath( "user-busy.png" );
54   default:
55     return IconManager::instance().iconPath( "user-online.png" );
56   }
57 }
58 
menuUserStatusIconFileName(int user_status)59 QString Bee::menuUserStatusIconFileName( int user_status )
60 {
61   switch( user_status )
62   {
63   case User::Offline:
64     return IconManager::instance().iconPath( "menu-user-offline.png" );
65   case User::Away:
66     return IconManager::instance().iconPath( "menu-user-away.png" );
67   case User::Busy:
68     return IconManager::instance().iconPath( "menu-user-busy.png" );
69   default:
70     return IconManager::instance().iconPath( "menu-user-online.png" );
71   }
72 }
73 
74 static const char* UserStatusToString[] =
75 {
76   QT_TRANSLATE_NOOP( "User", "offline" ),
77   QT_TRANSLATE_NOOP( "User", "available" ),
78   QT_TRANSLATE_NOOP( "User", "busy" ),
79   QT_TRANSLATE_NOOP( "User", "away" ),
80   QT_TRANSLATE_NOOP( "User", "status error" ),
81 };
82 
userStatusToString(int user_status)83 QString Bee::userStatusToString( int user_status )
84 {
85   if( user_status < 0 || user_status > User::NumStatus )
86     user_status = User::NumStatus;
87   return qApp->translate( "User", UserStatusToString[ user_status ] );
88 }
89 
userStatusColor(int user_status)90 QColor Bee::userStatusColor( int user_status )
91 {
92   switch( user_status )
93   {
94   case User::Online:
95     return QColor( Qt::green );
96   case User::Away:
97     return QColor( Qt::yellow );
98   case User::Busy:
99     return QColor( Qt::red );
100   case User::Offline:
101     return QColor( Qt::gray );
102   default:
103     return QColor( Qt::black );
104   }
105 }
106 
userStatusBackgroundColor(int user_status)107 QColor Bee::userStatusBackgroundColor( int user_status )
108 {
109   switch( user_status )
110   {
111   case User::Online:
112     return defaultBackgroundBrush().color();
113   case User::Away:
114     return QColor( Qt::darkYellow );
115   case User::Busy:
116     return QColor( Qt::darkRed );
117   case User::Offline:
118     return QColor( Qt::gray );
119   default:
120     return defaultBackgroundBrush().color();
121   }
122 }
123 
userStatusForegroundColor(int user_status)124 QColor Bee::userStatusForegroundColor( int user_status )
125 {
126   switch( user_status )
127   {
128   case User::Online:
129     return defaultBackgroundBrush().color();
130   case User::Away:
131     return QColor( Qt::white );
132   case User::Busy:
133     return QColor( Qt::white );
134   case User::Offline:
135     return QColor( Qt::black );
136   default:
137     return defaultBackgroundBrush().color();
138   }
139 }
140 
userNameToShow(const User & u,bool to_html)141 QString Bee::userNameToShow( const User& u, bool to_html )
142 {
143   QString user_name = Settings::instance().useUserFullName() && u.vCard().hasFullName() ? u.vCard().fullName( Settings::instance().useUserFirstNameFirstInFullName() ) : u.name();
144   if( Settings::instance().appendHostNameToUserName() && !u.localHostName().isEmpty() )
145     user_name.append( QString( " [%1]" ).arg( u.localHostName() ) );
146   return to_html ? Bee::replaceHtmlSpecialCharacters( user_name ) : user_name;
147 }
148 
uniqueFilePath(const QString & file_path,bool add_date_time)149 QString Bee::uniqueFilePath( const QString& file_path, bool add_date_time )
150 {
151   int counter = add_date_time ? 0 : 1;
152   QFileInfo fi( file_path );
153   QString dir_path = fi.absoluteDir().absolutePath();
154   QString file_base_name = fi.completeBaseName();
155   QString file_suffix = fi.suffix();
156   QString new_file_name;
157 
158   while( fi.exists() )
159   {
160     new_file_name = QString( "%1%2%3%4" )
161                       .arg( file_base_name )
162                       .arg( add_date_time ? QString( "-%1" ).arg( QDateTime::currentDateTime().toString( "yyyyMMddHHmmss" ) ) : QString( "" ) )
163                       .arg( counter > 0 ? ( add_date_time ? QString( "-%1" ).arg( counter ) : QString( " (%1)" ).arg( counter ) ) : QString( "" ) )
164                       .arg( file_suffix.isEmpty() ? QString( "" ) : QString( ".%1" ) ).arg( file_suffix );
165     fi.setFile( dir_path, new_file_name );
166     counter++;
167 
168     if( counter > 98 )
169     {
170       qWarning() << "Unable to find a unique file name from path" << file_path << "(so overwrite the last one)";
171       break;
172     }
173   }
174 
175   return QDir::toNativeSeparators( fi.absoluteFilePath() );
176 }
177 
suffixFromFile(const QString & file_path)178 QString Bee::suffixFromFile( const QString& file_path )
179 {
180   if( file_path.isEmpty() )
181     return "";
182   QStringList sl = file_path.split( "." );
183   if( sl.size() > 1 )
184     return sl.last();
185   else
186     return "";
187 }
188 
isFileTypeAudio(const QString & file_suffix)189 bool Bee::isFileTypeAudio( const QString& file_suffix )
190 {
191   QString sx = file_suffix.toLower();
192   return sx == "mp3" || sx == "wav" || sx == "wma" || sx == "flac" || sx == "aiff" || sx == "aac" || sx == "m4a" || sx == "m4p" ||
193          sx == "ogg" || sx == "oga" || sx == "ra" || sx == "rm";
194 }
195 
isFileTypeVideo(const QString & file_suffix)196 bool Bee::isFileTypeVideo( const QString& file_suffix )
197 {
198   QString sx = file_suffix.toLower();
199   return sx ==  "mpeg" || sx ==  "mpg" || sx == "mp4" || sx == "avi" || sx == "mkv" || sx == "wmv" || sx == "flv" || sx ==  "mov" ||
200          sx ==  "3gp" || sx ==  "mpe";
201 }
202 
isFileTypeImage(const QString & file_suffix)203 bool Bee::isFileTypeImage( const QString& file_suffix )
204 {
205   QString sx = file_suffix.toLower();
206   return sx == "jpg" || sx == "jpeg" || sx == "gif" || sx == "bmp" || sx == "png" || sx == "tiff" || sx == "tif" || sx == "psd" ||
207          sx == "nef" || sx == "cr2"  || sx == "dng" || sx == "dcr" || sx == "3fr" || sx == "raf" || sx == "orf" || sx == "pef" ||
208          sx == "arw" || sx == "svg" || sx == "ico" || sx == "ppm" || sx == "pgm" || sx == "pbm" || sx == "pnm" || sx == "webp";
209 }
210 
isFileTypeDocument(const QString & file_suffix)211 bool Bee::isFileTypeDocument( const QString& file_suffix )
212 {
213   QString sx = file_suffix.toLower();
214   return sx ==  "pdf" || sx.startsWith( "doc" ) || sx.startsWith( "xls" ) || sx.startsWith( "ppt" ) || sx.startsWith( "pps" ) ||
215          sx ==  "rtf" || sx ==  "txt" || sx ==  "odt" || sx ==  "odp" || sx ==  "ods" || sx ==  "csv" || sx ==  "log" ||
216          sx ==  "mobi" || sx ==  "epub";
217 }
218 
isFileTypeExe(const QString & file_suffix)219 bool Bee::isFileTypeExe( const QString& file_suffix )
220 {
221   QString sx = file_suffix.toLower();
222   return sx == "exe" || sx == "bat" || sx == "inf" || sx == "com" || sx == "sh" || sx == "cab" || sx == "cmd" || sx == "bin";
223 }
224 
isFileTypeBundle(const QString & file_suffix)225 bool Bee::isFileTypeBundle( const QString& file_suffix )
226 {
227   QString sx = file_suffix.toLower();
228   return sx == "app" || sx == "dmg";
229 }
230 
isFileTypeCompressed(const QString & file_suffix)231 bool Bee::isFileTypeCompressed( const QString& file_suffix )
232 {
233   QString sx = file_suffix.toLower();
234   return sx == "zip" || sx == "rar" || sx == "7z" || sx == "gz";
235 }
236 
fileTypeFromSuffix(const QString & file_suffix)237 Bee::FileType Bee::fileTypeFromSuffix( const QString& file_suffix )
238 {
239   if( isFileTypeDocument( file_suffix ) )
240     return Bee::FileDocument;
241 
242   if( isFileTypeImage( file_suffix ) )
243     return Bee::FileImage;
244 
245   if( isFileTypeAudio( file_suffix ) )
246     return Bee::FileAudio;
247 
248   if( isFileTypeVideo( file_suffix ) )
249     return Bee::FileVideo;
250 
251   if( isFileTypeCompressed( file_suffix ) )
252     return Bee::FileCompressed;
253 
254   if( isFileTypeExe( file_suffix ) )
255     return Bee::FileExe;
256 
257   if( isFileTypeBundle( file_suffix ) )
258     return Bee::FileBundle;
259 
260   return Bee::FileOther;
261 }
262 
263 static const char* FileTypeToString[] =
264 {
265   QT_TRANSLATE_NOOP( "File", "Audio" ),
266   QT_TRANSLATE_NOOP( "File", "Video" ),
267   QT_TRANSLATE_NOOP( "File", "Image" ),
268   QT_TRANSLATE_NOOP( "File", "Document" ),
269   QT_TRANSLATE_NOOP( "File", "Other" ),
270   QT_TRANSLATE_NOOP( "File", "Executable" ),
271   QT_TRANSLATE_NOOP( "File", "MacOSX" ),
272   QT_TRANSLATE_NOOP( "File", "Compressed" )
273 };
274 
fileTypeToString(Bee::FileType ft)275 QString Bee::fileTypeToString( Bee::FileType ft )
276 {
277   if( static_cast<int>( ft ) < 0 || ft > Bee::NumFileType )
278     ft = Bee::FileOther;
279   return qApp->translate( "File", FileTypeToString[ ft ] );
280 }
281 
282 static const char* ChatMessageTypeToString[] =
283 {
284   QT_TRANSLATE_NOOP( "ChatMessage", "Header" ),
285   QT_TRANSLATE_NOOP( "ChatMessage", "System" ),
286   QT_TRANSLATE_NOOP( "ChatMessage", "Chat" ),
287   QT_TRANSLATE_NOOP( "ChatMessage", "Connection" ),
288   QT_TRANSLATE_NOOP( "ChatMessage", "User Information" ),
289   QT_TRANSLATE_NOOP( "ChatMessage", "File Transfer" ),
290   QT_TRANSLATE_NOOP( "ChatMessage", "History" ),
291   QT_TRANSLATE_NOOP( "ChatMessage", "Other" ),
292   QT_TRANSLATE_NOOP( "ChatMessage", "Image Preview" ),
293   QT_TRANSLATE_NOOP( "ChatMessage", "Autoresponder" ),
294   QT_TRANSLATE_NOOP( "ChatMessage", "Voice message" )
295 };
296 
chatMessageTypeToString(int cmt)297 QString Bee::chatMessageTypeToString( int cmt )
298 {
299   if( cmt < 0 || cmt > ChatMessage::NumTypes )
300     cmt = ChatMessage::Other;
301   return qApp->translate( "ChatMessage", ChatMessageTypeToString[ cmt ] );
302 }
303 
dateTimeStringSuffix(const QDateTime & dt)304 QString Bee::dateTimeStringSuffix( const QDateTime& dt )
305 {
306   QString s = dt.toString( "yyyyMMdd-hhmmss" );
307   return s;
308 }
309 
capitalizeFirstLetter(const QString & txt,bool all_chars_after_space,bool lower_all_characters)310 QString Bee::capitalizeFirstLetter( const QString& txt, bool all_chars_after_space, bool lower_all_characters )
311 {
312   if( txt.isEmpty() )
313     return txt;
314 
315   QString capitalized = "";
316   QString tmp = lower_all_characters ? txt.toLower() : txt;
317   if( all_chars_after_space )
318   {
319     QChar c;
320     bool apply_title_case = true;
321     for( int i = 0; i < tmp.size(); i++ )
322     {
323       c = tmp.at( i );
324       if( c.isSpace() )
325       {
326         capitalized += c;
327         apply_title_case = true;
328       }
329       else if( apply_title_case )
330       {
331         capitalized += c.toTitleCase();
332         apply_title_case = false;
333       }
334       else
335         capitalized += c;
336     }
337   }
338   else
339   {
340     tmp.remove( 0, 1 );
341     capitalized += txt.at( 0 ).toTitleCase();
342     capitalized += tmp;
343   }
344   return capitalized;
345 }
346 
lowerFirstLetter(const QString & txt)347 QString Bee::lowerFirstLetter( const QString& txt )
348 {
349   if( !txt.isEmpty() && txt.at( 0 ).isUpper() )
350     return txt.at( 0 ).toLower() + txt.mid( 1 );
351   else
352     return txt;
353 }
354 
invertColor(const QColor & c)355 QColor Bee::invertColor( const QColor& c )
356 {
357   int r, g, b;
358   c.getRgb( &r, &g, &b );
359   int i_r = 255 - r;
360   int i_g = 255 - g;
361   int i_b = 255 - b;
362 
363   int s_r = r - i_r;
364   int s_g = g - i_g;
365   int s_b = b - i_b;
366 
367   if( qAbs( s_r ) < 30 && qAbs( s_g ) < 30 && qAbs( s_b ) < 30 ) // gray on gray
368     return QColor( 0, 0, 0 );
369   else
370     return QColor( i_r, i_g, i_b );
371 }
372 
isColorNear(const QColor & c1,const QColor & c2)373 bool Bee::isColorNear( const QColor& c1, const QColor& c2 )
374 {
375   int r_diff = c1.red() - c2.red();
376   int g_diff = c1.green() - c2.green();
377   int b_diff = c1.blue() - c2.blue();
378 
379   return qAbs( r_diff ) < 30 && qAbs( g_diff ) < 30 && qAbs( b_diff ) < 30;
380 }
381 
removeHtmlTags(const QString & s)382 QString Bee::removeHtmlTags( const QString& s )
383 {
384   QTextDocument text_document;
385   text_document.setHtml( s );
386   return text_document.toPlainText();
387 }
388 
defaultTextBrush()389 QBrush Bee::defaultTextBrush()
390 {
391   return qApp->palette().text();
392 }
393 
defaultBackgroundBrush()394 QBrush Bee::defaultBackgroundBrush()
395 {
396   return QBrush( Qt::transparent );
397 }
398 
defaultHighlightedTextBrush()399 QBrush Bee::defaultHighlightedTextBrush()
400 {
401   return qApp->palette().highlightedText();
402 }
403 
defaultHighlightBrush()404 QBrush Bee::defaultHighlightBrush()
405 {
406   return qApp->palette().highlight();
407 }
408 
userStatusBackgroundBrush(int user_status)409 QBrush Bee::userStatusBackgroundBrush( int user_status )
410 {
411   if( user_status == User::Away || user_status == User::Busy )
412     return QBrush( userStatusColor( user_status ) );
413   else
414     return defaultBackgroundBrush();
415 }
416 
convertToGrayScale(const QIcon & icon_to_convert,const QSize & pixmap_size)417 QPixmap Bee::convertToGrayScale( const QIcon& icon_to_convert, const QSize& pixmap_size )
418 {
419   return convertToGrayScale( icon_to_convert.pixmap( pixmap_size ) );
420 }
421 
convertToGrayScale(const QPixmap & pix)422 QPixmap Bee::convertToGrayScale( const QPixmap& pix )
423 {
424   QImage img = pix.toImage();
425   if( img.isNull() )
426     return QPixmap();
427 
428   int pixels = img.width() * img.height();
429   if( pixels*static_cast<int>( sizeof( QRgb ) ) <= img.byteCount() )
430   {
431     QRgb *data = reinterpret_cast<QRgb*>( img.bits() );
432     for (int i = 0; i < pixels; i++)
433     {
434       int val = qGray(data[i]);
435       data[i] = qRgba(val, val, val, qAlpha(data[i]));
436     }
437   }
438 
439   QPixmap ret_pix;
440 
441 #if QT_VERSION < 0x040700
442   ret_pix = QPixmap::fromImage( img );
443 #else
444   ret_pix.convertFromImage( img );
445 #endif
446   return ret_pix;
447 }
448 
nativeFolderSeparator()449 QChar Bee::nativeFolderSeparator()
450 {
451   return QDir::separator();
452 }
453 
convertToNativeFolderSeparator(const QString & raw_path)454 QString Bee::convertToNativeFolderSeparator( const QString& raw_path )
455 {
456   QString path_converted( raw_path );
457   QChar from_char;
458 #if defined( Q_OS_WIN ) || defined( Q_OS_OS2 ) || defined( Q_OS_OS2EMX ) || defined( Q_OS_SYMBIAN )
459   from_char = QLatin1Char( '/' );
460 #else
461   from_char = QLatin1Char( '\\' );
462 #endif
463   QChar to_char = QDir::separator();
464 
465   for( int i = 0; i < path_converted.length(); i++ )
466   {
467     if( path_converted[ i ] == from_char )
468       path_converted[ i ] = to_char;
469   }
470   // Do not remove "double slash" because some paths can start with them
471   // Remove trailing slash
472   if( path_converted.endsWith( QDir::separator() ) )
473     path_converted.chop( 1 );
474   return path_converted;
475 }
476 
folderCdUp(const QString & folder_path)477 QString Bee::folderCdUp( const QString& folder_path )
478 {
479   QStringList sl = Bee::convertToNativeFolderSeparator( folder_path ).split( nativeFolderSeparator() );
480   if( sl.isEmpty() )
481     return folder_path;
482 
483   sl.removeLast();
484   return sl.join( nativeFolderSeparator() );
485 }
486 
setLastModifiedToFile(const QString & to_path,const QDateTime & dt_last_modified)487 bool Bee::setLastModifiedToFile( const QString& to_path, const QDateTime& dt_last_modified )
488 {
489   QFileInfo file_info_to( to_path );
490   if( !file_info_to.exists() )
491   {
492     qWarning() << "Unable to set last modidified time to not existing file" << qPrintable( to_path );
493     return false;
494   }
495 
496   uint mod_time = dt_last_modified.toTime_t();
497   uint ac_time = dt_last_modified.toTime_t();
498 
499   if( mod_time == (uint)-1 || ac_time == (uint)-1 )
500   {
501     qWarning() << "Unable to set invalid last modidified time to file" << qPrintable( to_path );
502     return false;
503   }
504 
505   bool ok = false;
506 
507 #ifdef Q_OS_WIN
508 
509   uint cr_time = mod_time;
510 
511   FILETIME ft_modified, ft_creation, ft_access;
512 
513   LPCWSTR to_file_name = (const WCHAR*)to_path.utf16();
514 
515   HANDLE h_file = ::CreateFileW( to_file_name, GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL );
516 
517   if( h_file != INVALID_HANDLE_VALUE )
518   {
519     LONGLONG ll = Int32x32To64( cr_time, 10000000) + 116444736000000000;
520     ft_creation.dwLowDateTime = (DWORD) ll;
521     ft_creation.dwHighDateTime = ll >> 32;
522     LONGLONG ll2 = Int32x32To64( mod_time, 10000000) + 116444736000000000;
523     ft_modified.dwLowDateTime = (DWORD) ll2;
524     ft_modified.dwHighDateTime = ll2 >> 32;
525     LONGLONG ll3 = Int32x32To64( ac_time, 10000000) + 116444736000000000;
526     ft_access.dwLowDateTime = (DWORD) ll3;
527     ft_access.dwHighDateTime = ll3 >> 32;
528 
529     if( !::SetFileTime( h_file, &ft_creation, &ft_access, &ft_modified ) )
530     {
531       QString s_error = QString( "0x%1" ).arg( (unsigned long)GetLastError() );
532       qWarning() << "Function SetFileTime has error" << s_error << "for file" << qPrintable( to_path );
533     }
534     else
535       ok = true;
536   }
537   else
538     qWarning() << "Unable to get the HANDLE (CreateFile) of file" << qPrintable( to_path );
539 
540   CloseHandle( h_file );
541 
542 #else
543 
544   struct utimbuf from_time_buffer;
545   from_time_buffer.modtime = mod_time;
546   from_time_buffer.actime = ac_time;
547 
548   const char *to_file_name = to_path.toUtf8().constData();
549   ok = utime( to_file_name, &from_time_buffer ) == 0;
550   if( !ok )
551     qWarning() << "Function utime error" << errno << ":" << qPrintable( QString::fromLatin1( strerror( errno ) ) ) << "for file" << qPrintable( to_path );
552 
553 #endif
554 
555   return ok;
556 }
557 
showFileInGraphicalShell(const QString & file_path)558 bool Bee::showFileInGraphicalShell( const QString& file_path )
559 {
560   QFileInfo file_info( file_path );
561   if( !file_info.exists() )
562     return false;
563 
564 #ifdef Q_OS_WIN
565   QString explorer_path = QLatin1String( "c:\\windows\\explorer.exe" );
566 
567   if( QFile::exists( explorer_path ) )
568   {
569     QStringList explorer_args;
570     if( !file_info.isDir() )
571       explorer_args += QLatin1String("/select,");
572     explorer_args += Bee::convertToNativeFolderSeparator( file_info.canonicalFilePath() );
573     if( QProcess::startDetached( explorer_path, explorer_args ) )
574       return true;
575     else
576       qWarning() << "Unable to start process:" << qPrintable( explorer_path ) << qPrintable( explorer_args.join( " " ) );
577   }
578 #endif
579 
580 #ifdef Q_OS_MAC
581 
582   QStringList script_args;
583   script_args << QLatin1String( "-e" )
584               << QString::fromLatin1( "tell application \"Finder\" to reveal POSIX file \"%1\"" ).arg( file_info.canonicalFilePath() );
585   QProcess::execute( QLatin1String( "/usr/bin/osascript" ), script_args );
586   script_args.clear();
587   script_args << QLatin1String( "-e" )
588              << QLatin1String( "tell application \"Finder\" to activate" );
589   QProcess::execute( QLatin1String( "/usr/bin/osascript" ), script_args );
590   return true;
591 
592 #endif
593 
594   return false;
595 }
596 
folderIsWriteable(const QString & folder_path,bool create_folder_if_not_exists)597 bool Bee::folderIsWriteable( const QString& folder_path, bool create_folder_if_not_exists )
598 {
599   QDir folder_to_test( folder_path );
600   if( !folder_to_test.exists() )
601   {
602     if( create_folder_if_not_exists )
603       return folder_to_test.mkpath( "." );
604     else
605       return false;
606   }
607 
608   QFile test_file( QString( "%1/%2" ).arg( folder_path ).arg( "beetestfile.txt" ) );
609   if( test_file.open( QFile::WriteOnly ) )
610   {
611     bool ok = test_file.write( QByteArray( "BeeBEEP" ) ) > 0;
612     test_file.close();
613     return ok && test_file.remove();
614   }
615   else
616     return false;
617 }
618 
GetBoxSize(int pix_size)619 static int GetBoxSize( int pix_size )
620 {
621   int box_size = pix_size > 10 ? pix_size / 10 : 1;
622   if( box_size % 2 > 7 )
623     box_size++;
624   box_size = qMax( 1, box_size );
625   return box_size;
626 }
627 
avatarForUser(const User & u,const QSize & avatar_size,bool use_available_user_image,int user_status)628 QPixmap Bee::avatarForUser( const User& u, const QSize& avatar_size, bool use_available_user_image, int user_status )
629 {
630   QPixmap user_avatar;
631   bool default_avatar_used = false;
632   if( user_status < 0 )
633     user_status = u.status();
634   if( u.vCard().photo().isNull() || !use_available_user_image )
635   {
636     default_avatar_used = true;
637     Avatar av;
638     av.setName( u.isValid() ? (Settings::instance().useUserFullName() && u.vCard().hasFullName() ? u.vCard().fullName( Settings::instance().useUserFirstNameFirstInFullName() ) : u.name()) : "??" );
639     if( u.isStatusConnected() )
640       av.setColor( u.color() );
641     else
642       av.setColor( QColor( Qt::gray ).name() );
643     av.setSize( avatar_size );
644     if( !av.create() )
645     {
646       user_avatar = QIcon( Bee::menuUserStatusIconFileName( user_status ) ).pixmap( avatar_size );
647       return user_avatar;
648     }
649     else
650       user_avatar = av.pixmap();
651   }
652   else
653   {
654     user_avatar = u.vCard().photo().scaled( avatar_size );
655     if( !u.isStatusConnected() )
656     {
657       user_avatar = convertToGrayScale(user_avatar );
658       return user_avatar;
659     }
660   }
661 
662   int pix_height = user_avatar.height();
663   int pix_width = user_avatar.width();
664   int box_height = GetBoxSize( pix_height );
665   int box_width = GetBoxSize( pix_width );
666   int box_start_height = qMax( 1, box_height / 2 );
667   int box_start_width = qMax( 1, box_width / 2 );
668 
669   QPixmap pix( pix_width, pix_height );
670   QPainter p( &pix );
671   if( !default_avatar_used )
672   {
673     pix.fill( Bee::userStatusColor( user_status ) );
674     p.drawPixmap( box_start_width, box_start_height, pix_width - box_width, pix_height - box_height, user_avatar.scaled( pix_width - box_width, pix_height - box_height ) );
675   }
676   else
677   {
678     p.drawPixmap( 0, 0, pix_width, pix_height, user_avatar );
679     p.setPen( Bee::userStatusColor( user_status ) );
680     for( int i = 0; i < box_height; i++ )
681     {
682       p.drawLine( 0, i, pix_width, i );
683       p.drawLine( 0, pix_height-box_height+i, pix_width, pix_height-box_height+i );
684     }
685 
686     for( int i = 0; i < box_width; i++ )
687     {
688       p.drawLine( i, 0, i, pix_height );
689       p.drawLine( pix_width-box_width+i, 0, pix_width-box_width+i, pix_height );
690     }
691   }
692 
693   return pix;
694 }
695 
toolTipForUser(const User & u,bool only_status)696 QString Bee::toolTipForUser( const User& u, bool only_status )
697 {
698   QString tool_tip = u.isLocal() ? QObject::tr( "You are %1" ).arg( Bee::userStatusToString( u.status() ) ) : QObject::tr( "%1 is %2" ).arg( Bee::userNameToShow( u, false ), Bee::userStatusToString( u.status() ) );
699 
700   if( only_status )
701     return tool_tip;
702 
703   if( !u.vCard().birthday().isNull() )
704   {
705     QString text = userBirthdayToText( u );
706     if( !text.isEmpty() )
707       tool_tip +=  QString( "\n* %1 *" ).arg( text );
708   }
709 
710   if( u.isStatusConnected() )
711   {
712     if( !u.statusDescription().isEmpty() )
713       tool_tip += QString( "\n%1" ).arg( u.statusDescription() );
714 
715     if( !u.workgroups().isEmpty() )
716       tool_tip += QString( "\n%1: %2" ).arg( QObject::tr( "Workgroups" ) ).arg( Bee::stringListToTextString( u.workgroups(), true, 9 ) );
717 
718     if( !u.vCard().info().isEmpty() )
719     {
720       tool_tip += QString( "\n~~~\n" );
721       tool_tip += u.vCard().info();
722       tool_tip += QString( "\n~~~" );
723     }
724 
725     if( u.statusChangedIn().isValid() )
726       tool_tip += QString( "\n%1: %2" ).arg( QObject::tr( "Last update" ) ).arg( Bee::dateTimeToString( u.statusChangedIn() ) );
727   }
728   else
729   {
730     if( u.lastConnection().isValid() )
731       tool_tip += QString( "\n%1: %2" ).arg( QObject::tr( "Last connection" ) ).arg( Bee::dateTimeToString( u.lastConnection() ) );
732 
733     int unsent_messages = MessageManager::instance().countMessagesToSendToUserId( u.id() );
734     if( unsent_messages > 0 )
735       tool_tip += QString( "\n%1" ).arg( QObject::tr( "%n unsent message(s)", "", unsent_messages ) );
736   }
737   tool_tip += QString( "\n" );
738   return tool_tip;
739 }
740 
userBirthdayToText(const User & u)741 QString Bee::userBirthdayToText( const User& u )
742 {
743   QString birthday_text;
744   if( !u.isLocal() )
745   {
746     // Do not use Bee::userNameToShow( u ) to avoid computer name
747     QString user_name = Settings::instance().useUserFullName() && u.vCard().hasFullName() ? u.vCard().fullName( Settings::instance().useUserFirstNameFirstInFullName() ) : u.name();
748     user_name = Bee::removeHtmlTags( user_name );
749     user_name = Bee::replaceHtmlSpecialCharacters( user_name );
750     int days_to = u.daysToBirthDay();
751     if( days_to == 0 )
752       birthday_text = QObject::tr( "Today is %1's birthday" ).arg( user_name );
753     else if( days_to == 1 )
754       birthday_text =  QObject::tr( "Tomorrow is %1's birthday" ).arg( user_name );
755     else if( days_to > 1 && days_to < 4 )
756       birthday_text= QObject::tr( "%1's birthday is in %2 days" ).arg( user_name ).arg( days_to );
757     else if( days_to == -1 )
758       birthday_text = QObject::tr( "Yesterday was %1's birthday" ).arg( user_name );
759     else
760       birthday_text = "";
761   }
762   else
763   {
764     if( u.isBirthDay() )
765       birthday_text = QObject::tr( "Happy Birthday to you!" );
766     else
767       birthday_text = "";
768   }
769   return birthday_text;
770 }
771 
stringListToTextString(const QStringList & sl,bool strip_html_tags,int max_items)772 QString Bee::stringListToTextString( const QStringList& sl, bool strip_html_tags, int max_items )
773 {
774   QStringList sl_parsed;
775   foreach( QString s, sl )
776   {
777     if( !s.isEmpty() )
778     {
779       if( strip_html_tags )
780         sl_parsed.append( Bee::removeHtmlTags( s ) );
781       else
782         sl_parsed.append( s );
783     }
784   }
785 
786   if( sl_parsed.isEmpty() )
787     return "";
788   if( sl_parsed.size() == 1 )
789     return sl_parsed.first();
790   if( sl_parsed.size() == 2 )
791     return sl_parsed.join( QString( " %1 " ).arg( QObject::tr( "and" ) ) );
792 
793   QStringList sl_to_join;
794   if( max_items < 1 || max_items > (sl_parsed.size()-1))
795     max_items = sl_parsed.size()-1;
796   int num_items = 0;
797 
798   foreach( QString s, sl_parsed )
799   {
800     if( num_items >= max_items )
801       break;
802     num_items++;
803     if( strip_html_tags )
804       sl_to_join.append( Bee::removeHtmlTags( s ) );
805     else
806       sl_to_join.append( s );
807   }
808 
809   QString s_joined = sl_to_join.join( ", " );
810   int diff_items = sl_parsed.size() - sl_to_join.size();
811   if( diff_items == 1 )
812   {
813     if( !sl_parsed.last().isEmpty() )
814     {
815       s_joined.append( QString( " %1 " ).arg( QObject::tr( "and" ) ) );
816       s_joined.append( sl_parsed.last() );
817     }
818   }
819   else
820     s_joined.append( QString( " %1" ).arg( QObject::tr( "and %1 others" ).arg( diff_items ) ) );
821 
822   return s_joined;
823 }
824 
removeContextHelpButton(QWidget * w)825 void Bee::removeContextHelpButton( QWidget* w )
826 {
827   Qt::WindowFlags w_flags = w->windowFlags();
828   w_flags &= ~Qt::WindowContextHelpButtonHint;
829   w->setWindowFlags( w_flags );
830 }
831 
showUp(QWidget * w)832 void Bee::showUp( QWidget* w )
833 {
834   if( !w->isVisible() )
835     w->show();
836 
837   if( w->isMinimized() )
838     w->showNormal();
839 
840   w->raise();
841 }
842 
raiseOnTop(QWidget * w)843 void Bee::raiseOnTop( QWidget* w )
844 {
845 #if defined( Q_OS_WIN )
846   Bee::showUp( w );
847   if( !(w->windowFlags() & Qt::WindowStaysOnTopHint) )
848   {
849     ::SetWindowPos( (HWND)w->winId(), HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE );
850     ::SetWindowPos( (HWND)w->winId(), HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE );
851   }
852 #elif defined( Q_OS_LINUX ) && QT_VERSION >= 0x050000
853   Bee::showUp( w );
854   bool on_top_flag_added = false;
855   if( !(w->windowFlags() & Qt::WindowStaysOnTopHint) )
856   {
857     Bee::setWindowStaysOnTop( w, true );
858     on_top_flag_added = true;
859     w->raise();
860   }
861   if( on_top_flag_added )
862     Bee::setWindowStaysOnTop( w, false );
863 #else
864   bool on_top_flag_added = false;
865   if( !(w->windowFlags() & Qt::WindowStaysOnTopHint) )
866   {
867     Bee::setWindowStaysOnTop( w, true );
868     on_top_flag_added = true;
869   }
870   Bee::showUp( w );
871   if( on_top_flag_added )
872     Bee::setWindowStaysOnTop( w, false );
873 #endif
874 }
875 
setWindowStaysOnTop(QWidget * w,bool enable)876 void Bee::setWindowStaysOnTop( QWidget* w, bool enable )
877 {
878   bool w_was_visible = w->isVisible();
879   if( w_was_visible )
880     w->hide();
881 
882   Qt::WindowFlags w_flags = w->windowFlags();
883   if( enable )
884     w_flags |= Qt::WindowStaysOnTopHint;
885   else
886     w_flags &= ~Qt::WindowStaysOnTopHint;
887   w->setWindowFlags( w_flags );
888 
889   if( w_was_visible )
890     QMetaObject::invokeMethod( w, "show", Qt::QueuedConnection );
891 }
892 
areStringListEqual(const QStringList & sl1,const QStringList & sl2,Qt::CaseSensitivity cs)893 bool Bee::areStringListEqual( const QStringList& sl1, const QStringList& sl2, Qt::CaseSensitivity cs )
894 {
895   if( sl1.size() != sl2.size() )
896     return false;
897 
898   foreach( QString s, sl1 )
899   {
900     if( !sl2.contains( s, cs ) )
901       return false;
902   }
903   return true;
904 }
905 
dateTimeToString(const QDateTime & dt)906 QString Bee::dateTimeToString( const QDateTime& dt )
907 {
908   return dt.date() == QDate::currentDate() ? dt.time().toString( Qt::SystemLocaleShortDate ) : dt.toString( Qt::SystemLocaleShortDate );
909 }
910 
setBackgroundColor(QWidget * w,const QColor & c)911 void Bee::setBackgroundColor( QWidget* w, const QColor& c )
912 {
913   QPalette pal = w->palette();
914   pal.setBrush( QPalette::Base, QBrush( c ) );
915   w->setPalette( pal );
916 }
917 
selectColor(QWidget * w,const QColor & default_color)918 QColor Bee::selectColor( QWidget* w, const QColor& default_color )
919 {
920   return QColorDialog::getColor( default_color, w );
921 }
922 
pluginFileExtension()923 QString Bee::pluginFileExtension()
924 {
925 #if defined Q_OS_MAC
926   return QLatin1String( "dylib" );
927 #elif defined Q_OS_UNIX
928   return QLatin1String( "so" );
929 #else
930   return QLatin1String( "dll" );
931 #endif
932 }
933 
removeInvalidCharactersForFilePath(const QString & s)934 QString Bee::removeInvalidCharactersForFilePath( const QString& s )
935 {
936   QString valid_path = s.simplified();
937   valid_path.replace( QRegExp( "[" + QRegExp::escape( "\\/:*?\"<>|$[]+,;=" ) + "]" ), QString( "_" ) );
938   return valid_path;
939 }
940 
replaceHtmlSpecialCharacters(const QString & s)941 QString Bee::replaceHtmlSpecialCharacters( const QString& s )
942 {
943   // It is different from Protocol::formatHtmlText(...)
944   QString text = s.trimmed();
945   QString html_text = "";
946   bool there_is_a_space_before_it = false;
947   QChar c;
948 
949   for( int i = 0; i < text.length(); i++ )
950   {
951     c = text.at( i );
952     if( c == QLatin1Char( ' ' ) )
953     {
954       if( there_is_a_space_before_it )
955         html_text += QLatin1String( "&nbsp;" );
956       else
957         html_text += QLatin1Char( ' ' );
958       there_is_a_space_before_it = true;
959     }
960     else
961     {
962       there_is_a_space_before_it = false;
963       if( c == QLatin1Char( '\n' ) )
964         html_text += QLatin1String( "<br>" );
965       else if( c == QLatin1Char( '<' ) )
966         html_text += QLatin1String( "&lt;" );
967       else if( c == QLatin1Char( '>' ) )
968         html_text += QLatin1String( "&gt;" );
969       else if( c == QLatin1Char( '\t' ) )
970         html_text += QLatin1String( "&nbsp;&nbsp;" );
971       else
972         html_text += c;
973     }
974   }
975   return html_text;
976 }
977 
roundFromDouble(double d)978 qint64 Bee::roundFromDouble( double d )
979 {
980   qint64 i = static_cast<qint64>( d );
981   return ((d - i) >= 0.5) ? ++i : i;
982 }
983 
bytesPerSecond(FileSizeType transferred_byte_size,int time_elapsed_ms)984 qint64 Bee::bytesPerSecond( FileSizeType transferred_byte_size, int time_elapsed_ms )
985 {
986   double dtbs = static_cast<double>( transferred_byte_size );
987   double dte = static_cast<double>( time_elapsed_ms );
988   if( dtbs > 0 && dte > 0 )
989     return qMax( static_cast<qint64>(1), roundFromDouble( (dtbs*1000.0)/dte ) );
990   else
991     return 1;
992 }
993 
bytesToString(FileSizeType bytes,int precision)994 QString Bee::bytesToString( FileSizeType bytes, int precision )
995 {
996   QString suffix;
997   double result = 0;
998   int prec = 1;
999   if( bytes > 1000000000000 )
1000   {
1001     suffix = "Tb";
1002     result = bytes / 1000000000000.0;
1003     prec = 5;
1004   }
1005   else if( bytes > 1000000000 )
1006   {
1007     suffix = "Gb";
1008     result = bytes / 1000000000.0;
1009     prec = 3;
1010   }
1011   else if( bytes > 1000000 )
1012   {
1013     suffix = "Mb";
1014     result = bytes / 1000000.0;
1015     prec = 2;
1016   }
1017   else if( bytes > 1000 )
1018   {
1019     suffix = "kb";
1020     result = bytes / 1000.0;
1021     prec = result >= 10 ? 0 : 1;
1022   }
1023   else
1024   {
1025     suffix = "b";
1026     result = bytes;
1027     prec = 0;
1028   }
1029   return QString( "%1 %2").arg( result, 0, 'f', prec > 0 ? (precision >= 0 ? precision : prec) : 0 ).arg( suffix );
1030 }
1031 
timeToString(qint64 ms)1032 QString Bee::timeToString( qint64 ms )
1033 {
1034   if( ms < 0 )
1035     return QT_TRANSLATE_NOOP( "Not available", "n.a." );
1036   if( ms == 0 )
1037     return QString( "0 s" );
1038 
1039   qint64 d = ms / 86400000;
1040   if( d > 0 )
1041     ms -= d*86400000;
1042 
1043   QTime t = QTime( 0, 0, 0, 0 );
1044   t = t.addMSecs( static_cast<int>( ms ) );
1045   qint64 h = d * 24 + t.hour();
1046 
1047   if( h < 24 )
1048   {
1049     if( h > 0 )
1050       return t.toString( "%1:mm:ss" ).arg( QString::number( h ).rightJustified( 2, QLatin1Char( '0' ) ) );
1051     else if( t.minute() > 0 )
1052       return t.toString( "m:ss" ) + QString( " m" );
1053     else if( t.second() > 0 )
1054       return QString( "%1 s" ).arg( (t.msec() > 500 && t.second() <= 59 ? t.second() + 1 : t.second()) );
1055     else
1056       return QString( "%1 ms" ).arg( t.msec() );
1057   }
1058   else
1059   {
1060     QStringList sl;
1061     if( d > 0 )
1062       sl.append( QString( "%1 d" ).arg( d ) );
1063     if( t.hour() > 0 )
1064       sl.append( QString( "%1 h" ).arg( t.hour() ) );
1065     if( t.minute() > 0 )
1066       sl.append( QString( "%1 m" ).arg( t.minute() ) );
1067     if( t.second() > 0 )
1068       sl.append( QString( "%1 s" ).arg( t.second() ) );
1069     if( sl.empty() )
1070       sl.append( QString( "%1 ms" ).arg( t.msec() ) );
1071     return sl.join( ", " );
1072   }
1073 }
1074 
transferTimeLeft(FileSizeType bytes_transferred,FileSizeType total_bytes,FileSizeType starting_position,int elapsed_time)1075 QString Bee::transferTimeLeft( FileSizeType bytes_transferred, FileSizeType total_bytes, FileSizeType starting_position, int elapsed_time )
1076 {
1077   qint64 bytes_per_second;
1078   if( starting_position > 0 && bytes_transferred > starting_position )
1079     bytes_per_second = bytesPerSecond( bytes_transferred - starting_position, elapsed_time );
1080   else
1081     bytes_per_second = bytesPerSecond( bytes_transferred, elapsed_time );
1082   FileSizeType bytes_left = bytes_transferred >= total_bytes ? 0 : total_bytes - bytes_transferred;
1083   int ms_left = static_cast<int>( (bytes_left * 1000) / bytes_per_second );
1084   if( ms_left < 1000 )
1085     return timeToString( 1000 );
1086   else if( ms_left < 2000 )
1087     return timeToString( 2000 );
1088   else
1089     return timeToString( ms_left );
1090 }
1091 
imagePreviewPath(const QString & source_image_path)1092 QString Bee::imagePreviewPath( const QString& source_image_path )
1093 {
1094   QFileInfo fi( source_image_path );
1095   if( fi.exists() && fi.isReadable() )
1096   {
1097     QString file_png_path = QString( "%1-%2.png" ).arg( "img" ).arg( Protocol::instance().fileInfoHash( fi ) );
1098     QString image_preview_path = Bee::convertToNativeFolderSeparator( QString( "%1/%2" ).arg( Settings::instance().cacheFolder() ).arg( file_png_path ) );
1099     if( QFile::exists( image_preview_path ) )
1100     {
1101 #ifdef BEEBEEP_DEBUG
1102       qDebug() << "Image preview of" << source_image_path << "cached in file" << qPrintable( image_preview_path );
1103 #endif
1104       return image_preview_path;
1105     }
1106     QImage img;
1107     QImageReader img_reader( source_image_path );
1108     img_reader.setAutoDetectImageFormat( true );
1109     if( img_reader.read( &img ) )
1110     {
1111       if( img.height() > Settings::instance().imagePreviewHeight() )
1112       {
1113 #ifdef BEEBEEP_DEBUG
1114         qDebug() << "Image preview scaled and saving to" << qPrintable( image_preview_path );
1115 #endif
1116         // PNG for transparency (always)
1117         QImage img_scaled = img.scaledToHeight( Settings::instance().imagePreviewHeight(), Qt::SmoothTransformation );
1118         if( img_scaled.save( image_preview_path, "png" ) )
1119           return image_preview_path;
1120 #ifdef BEEBEEP_DEBUG
1121         else
1122           qDebug() << "Unable to save scaled image preview to" << qPrintable( image_preview_path );
1123 #endif
1124       }
1125       else
1126         return source_image_path;
1127     }
1128   }
1129   return QString( "" );
1130 }
1131 
comboBoxData(const QComboBox * box)1132 QVariant Bee::comboBoxData( const QComboBox* box )
1133 {
1134   int box_index = box->currentIndex();
1135   return box_index == -1 ? QVariant() : box->itemData( box_index );
1136 }
1137 
selectComboBoxData(QComboBox * box,const QVariant & item_data)1138 bool Bee::selectComboBoxData( QComboBox* box, const QVariant& item_data )
1139 {
1140   int box_index = box->findData( item_data );
1141   if( box_index >= 0 )
1142   {
1143     box->setCurrentIndex( box_index );
1144     return true;
1145   }
1146   else
1147     return false;
1148 }
1149 
colorDarkGrey()1150 QColor Bee::colorDarkGrey() { return QColor( 64, 64, 64 ); }
colorGrey()1151 QColor Bee::colorGrey() { return QColor( 128, 128, 128 ); }
colorBlack()1152 QColor Bee::colorBlack() { return QColor( 25, 25, 25 ); }
colorBlue()1153 QColor Bee::colorBlue() { return QColor( 42, 130, 218 ); }
colorWhite()1154 QColor Bee::colorWhite() { return QColor( 238, 238, 238 ); }
colorYellow()1155 QColor Bee::colorYellow() { return QColor( "#dede00" ); }
colorOrange()1156 QColor Bee::colorOrange() { return QColor( "#ffcf04" ); }
1157 
1158 
beeColorsToHtmlText(const QString & txt)1159 QString Bee::beeColorsToHtmlText( const QString& txt )
1160 {
1161   QString bee_txt = "";
1162   QString c_y = colorYellow().name();
1163   for( int i = 0; i < txt.size(); i++ )
1164     bee_txt.append( QString( "<font color=%1>%2</font>" ).arg( (i % 2 == 0) ? "#000000" : c_y ).arg( txt.at( i ) ) );
1165   return bee_txt;
1166 }
1167 
darkPalette()1168 QPalette Bee::darkPalette()
1169 {
1170   QPalette darkPalette;
1171   darkPalette.setColor( QPalette::Window, colorGrey() );
1172   darkPalette.setColor( QPalette::WindowText, colorWhite() );
1173   darkPalette.setColor( QPalette::Base, colorDarkGrey() );
1174   darkPalette.setColor( QPalette::AlternateBase, colorDarkGrey().lighter() );
1175   darkPalette.setColor( QPalette::ToolTipBase, colorGrey().lighter() );
1176   darkPalette.setColor( QPalette::ToolTipText, colorBlack() );
1177   darkPalette.setColor( QPalette::Text, colorWhite() );
1178   darkPalette.setColor( QPalette::BrightText, Qt::yellow );
1179   darkPalette.setColor( QPalette::Button, colorDarkGrey() );
1180   darkPalette.setColor( QPalette::ButtonText, colorWhite() );
1181   darkPalette.setColor( QPalette::Link, colorBlue() );
1182   darkPalette.setColor( QPalette::LinkVisited, colorBlue().darker() );
1183   darkPalette.setColor( QPalette::Highlight, colorGrey() );
1184   darkPalette.setColor( QPalette::HighlightedText, colorBlack() );
1185 
1186   darkPalette.setColor( QPalette::Active, QPalette::Button, colorGrey() );
1187   darkPalette.setColor( QPalette::Disabled, QPalette::ButtonText, colorGrey() );
1188   darkPalette.setColor( QPalette::Disabled, QPalette::WindowText, colorGrey() );
1189   darkPalette.setColor( QPalette::Disabled, QPalette::Text, colorDarkGrey() );
1190   darkPalette.setColor( QPalette::Disabled, QPalette::Light, colorGrey() );
1191 
1192   return darkPalette;
1193 }
1194