~ubuntu-branches/debian/jessie/armory/jessie

« back to all changes in this revision

Viewing changes to qtdefines.py

  • Committer: Package Import Robot
  • Author(s): Joseph Bisch
  • Date: 2014-10-07 10:22:45 UTC
  • Revision ID: package-import@ubuntu.com-20141007102245-2s3x3rhjxg689hek
Tags: upstream-0.92.3
ImportĀ upstreamĀ versionĀ 0.92.3

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
################################################################################
 
2
#                                                                              #
 
3
# Copyright (C) 2011-2014, Armory Technologies, Inc.                           #
 
4
# Distributed under the GNU Affero General Public License (AGPL v3)            #
 
5
# See LICENSE or http://www.gnu.org/licenses/agpl.html                         #
 
6
#                                                                              #
 
7
################################################################################
 
8
import struct
 
9
from tempfile import mkstemp
 
10
 
 
11
from PyQt4.QtCore import *
 
12
from PyQt4.QtGui import *
 
13
import urllib
 
14
 
 
15
from armorycolors import Colors, htmlColor
 
16
from armoryengine.ArmoryUtils import *
 
17
from armoryengine.BinaryUnpacker import *
 
18
from armoryengine.MultiSigUtils import *
 
19
 
 
20
 
 
21
SETTINGS_PATH   = os.path.join(ARMORY_HOME_DIR, 'ArmorySettings.txt')
 
22
USERMODE        = enum('Standard', 'Advanced', 'Expert')
 
23
SATOSHIMODE     = enum('Auto', 'User')
 
24
NETWORKMODE     = enum('Offline', 'Full', 'Disconnected')
 
25
WLTTYPES        = enum('Plain', 'Crypt', 'WatchOnly', 'Offline')
 
26
WLTFIELDS       = enum('Name', 'Descr', 'WltID', 'NumAddr', 'Secure', \
 
27
                       'BelongsTo', 'Crypto', 'Time', 'Mem', 'Version')
 
28
MSGBOX          = enum('Good','Info', 'Question', 'Warning', 'Critical', 'Error')
 
29
MSGBOX          = enum('Good','Info', 'Question', 'Warning', 'Critical', 'Error')
 
30
DASHBTNS        = enum('Close', 'Browse', 'Install', 'Instruct', 'Settings')
 
31
 
 
32
STYLE_SUNKEN = QFrame.Box | QFrame.Sunken
 
33
STYLE_RAISED = QFrame.Box | QFrame.Raised
 
34
STYLE_PLAIN  = QFrame.Box | QFrame.Plain
 
35
STYLE_STYLED = QFrame.StyledPanel | QFrame.Raised
 
36
STYLE_NONE   = QFrame.NoFrame
 
37
VERTICAL = 'vertical'
 
38
HORIZONTAL = 'horizontal'
 
39
CHANGE_ADDR_DESCR_STRING = '[[ Change received ]]'
 
40
HTTP_VERSION_FILE = 'https://bitcoinarmory.com/versions.txt'
 
41
BUG_REPORT_URL = 'https://bitcoinarmory.com/scripts/receive_debug.php'
 
42
PRIVACY_URL = 'https://bitcoinarmory.com/privacy-policy'
 
43
# For announcements handling
 
44
ANNOUNCE_FETCH_INTERVAL = 1 * HOUR
 
45
if CLI_OPTIONS.testAnnounceCode:
 
46
   HTTP_ANNOUNCE_FILE = \
 
47
      'https://s3.amazonaws.com/bitcoinarmory-testing/testannounce.txt'
 
48
else:
 
49
   HTTP_ANNOUNCE_FILE = 'https://bitcoinarmory.com/atiannounce.txt'
 
50
 
 
51
# Keep track of dialogs and wizard that are executing
 
52
runningDialogsList = []
 
53
 
 
54
def AddToRunningDialogsList(func):
 
55
   def wrapper(*args, **kwargs):
 
56
      runningDialogsList.append(args[0])
 
57
      result = func(*args, **kwargs)
 
58
      runningDialogsList.remove(args[0])
 
59
      return result
 
60
   return wrapper
 
61
 
 
62
################################################################################
 
63
def tr(txt, replList=None, pluralList=None):
 
64
   """
 
65
   This is a common convention for implementing translations, where all 
 
66
   translatable strings are put int the _(...) function, and that method 
 
67
   does some fancy stuff to present the translation if needed. 
 
68
 
 
69
   This is being implemented here, to not only do translations in the 
 
70
   future, but also to clean up the typical text fields I use.  I've 
 
71
   ended up with a program full of stuff like this:
 
72
 
 
73
      myLabel = QRichLabel( \
 
74
         'This text is split across mulitple lines '
 
75
         'with a space after each one, and single '
 
76
         'quotes on either side.')
 
77
   
 
78
   Instead it should really look like: 
 
79
      
 
80
      myLabel = QRichLabel( tr('''
 
81
         This text is split across mulitple lines 
 
82
         and it will acquire a space after each line 
 
83
         as well as include newlines because it's HTML
 
84
         and uses <br>. ''' ))
 
85
 
 
86
   Added easy plural handling:
 
87
 
 
88
      Just add an extra argument to specify a variable on which plurality
 
89
      should be chosen, and then decorate your text with 
 
90
 
 
91
         @{singular|plural}@
 
92
   
 
93
   For instance:
 
94
 
 
95
      tr('The @{cat|cats}@ danced.  @{It was|They were}@ happy.', nCat)
 
96
      tr('The @{cat|%d cats}@ danced.  @{It was|They were}@ happy.'%nCat, nCat)
 
97
      tr('The @{cat|cats}@ attacked the @{dog|dogs}@', nCat, nDog)
 
98
 
 
99
   This should work well for 
 
100
   """
 
101
 
 
102
   txt = toUnicode(txt)
 
103
   lines = [l.strip() for l in txt.split('\n')]
 
104
   txt = (' '.join(lines)).strip()
 
105
 
 
106
   # Eventually we do something cool with this transalate function.
 
107
   # It will be defined elsewhere, but for now stubbed with identity fn
 
108
   TRANSLATE = lambda x: x
 
109
 
 
110
   txt = TRANSLATE(txt)
 
111
 
 
112
   return formatWithPlurals(txt, replList, pluralList)
 
113
   
 
114
 
 
115
 
 
116
################################################################################
 
117
def HLINE(style=QFrame.Plain):
 
118
   qf = QFrame()
 
119
   qf.setFrameStyle(QFrame.HLine | style)
 
120
   return qf
 
121
 
 
122
def VLINE(style=QFrame.Plain):
 
123
   qf = QFrame()
 
124
   qf.setFrameStyle(QFrame.VLine | style)
 
125
   return qf
 
126
 
 
127
 
 
128
 
 
129
# Setup fixed-width and var-width fonts
 
130
def GETFONT(ftype, sz=10, bold=False, italic=False):
 
131
   fnt = None
 
132
   if ftype.lower().startswith('fix'):
 
133
      if OS_WINDOWS:
 
134
         fnt = QFont("Courier", sz)
 
135
      elif OS_MACOSX:
 
136
         fnt = QFont("Menlo", sz)
 
137
      else: 
 
138
         fnt = QFont("DejaVu Sans Mono", sz)
 
139
   elif ftype.lower().startswith('var'):
 
140
      if OS_MACOSX:
 
141
         fnt = QFont("Lucida Grande", sz)
 
142
      else:
 
143
         fnt = QFont("Verdana", sz)
 
144
      #if OS_WINDOWS:
 
145
         #fnt = QFont("Tahoma", sz)
 
146
      #else: 
 
147
         #fnt = QFont("Sans", sz)
 
148
   elif ftype.lower().startswith('money'):
 
149
      if OS_WINDOWS:
 
150
         fnt = QFont("Courier", sz)
 
151
      elif OS_MACOSX:
 
152
         fnt = QFont("Menlo", sz)
 
153
      else: 
 
154
         fnt = QFont("DejaVu Sans Mono", sz)
 
155
   else:
 
156
      fnt = QFont(ftype, sz)
 
157
 
 
158
   if bold:
 
159
      fnt.setWeight(QFont.Bold)
 
160
 
 
161
   if italic:
 
162
      fnt.setItalic(True)
 
163
   
 
164
   return fnt
 
165
      
 
166
 
 
167
def UnicodeErrorBox(parent):
 
168
   QMessageBox.warning(parent, 'ASCII Error', \
 
169
      toUnicode('Armory does not currently support non-ASCII characters in '
 
170
      'most text fields (like \xc2\xa3\xc2\xa5\xc3\xa1\xc3\xb6\xc3\xa9).  '
 
171
      'Please use only letters found '
 
172
      'on an English(US) keyboard.  This will be fixed in an upcoming '
 
173
      'release'), QMessageBox.Ok)
 
174
 
 
175
 
 
176
 
 
177
 
 
178
#######
 
179
def UserModeStr(mode):
 
180
   if mode==USERMODE.Standard:
 
181
      return 'Standard'
 
182
   elif mode==USERMODE.Advanced:
 
183
      return 'Advanced'
 
184
   elif mode==USERMODE.Expert:
 
185
      return 'Expert'
 
186
 
 
187
 
 
188
#######
 
189
def tightSizeNChar(obj, nChar):
 
190
   """ 
 
191
   Approximates the size of a row text of mixed characters
 
192
 
 
193
   This is only aproximate, since variable-width fonts will vary
 
194
   depending on the specific text
 
195
   """
 
196
 
 
197
   try:
 
198
      fm = QFontMetricsF(QFont(obj.font()))
 
199
   except AttributeError:
 
200
      fm = QFontMetricsF(QFont(obj))
 
201
   szWidth,szHeight = fm.boundingRect('abcfgijklm').width(), fm.height()
 
202
   szWidth = int(szWidth * nChar/10.0 + 0.5)
 
203
   return szWidth, szHeight
 
204
 
 
205
#######
 
206
def tightSizeStr(obj, theStr):
 
207
   """ Measure a specific string """
 
208
   try:
 
209
      fm = QFontMetricsF(QFont(obj.font()))
 
210
   except AttributeError:
 
211
      fm = QFontMetricsF(QFont(obj))
 
212
   szWidth,szHeight = fm.boundingRect(theStr).width(), fm.height()
 
213
   return szWidth, szHeight
 
214
   
 
215
#######
 
216
def relaxedSizeStr(obj, theStr):
 
217
   """
 
218
   Approximates the size of a row text, nchars long, adds some margin
 
219
   """
 
220
   try:
 
221
      fm = QFontMetricsF(QFont(obj.font()))
 
222
   except AttributeError:
 
223
      fm = QFontMetricsF(QFont(obj))
 
224
   szWidth,szHeight = fm.boundingRect(theStr).width(), fm.height()
 
225
   return (10 + szWidth*1.05), 1.5*szHeight
 
226
 
 
227
#######
 
228
def relaxedSizeNChar(obj, nChar):
 
229
   """
 
230
   Approximates the size of a row text, nchars long, adds some margin
 
231
   """
 
232
   try:
 
233
      fm = QFontMetricsF(QFont(obj.font()))
 
234
   except AttributeError:
 
235
      fm = QFontMetricsF(QFont(obj))
 
236
   szWidth,szHeight = fm.boundingRect('abcfg ijklm').width(), fm.height()
 
237
   szWidth = int(szWidth * nChar/10.0 + 0.5)
 
238
   return (10 + szWidth*1.05), 1.5*szHeight
 
239
 
 
240
#############################################################################
 
241
def determineWalletType(wlt, wndw):
 
242
   if wlt.watchingOnly:
 
243
      if wndw.getWltSetting(wlt.uniqueIDB58, 'IsMine'):
 
244
         return [WLTTYPES.Offline, 'Offline']
 
245
      else:
 
246
         return [WLTTYPES.WatchOnly, 'Watching-Only']
 
247
   elif wlt.useEncryption:
 
248
      return [WLTTYPES.Crypt, 'Encrypted']
 
249
   else:
 
250
      return [WLTTYPES.Plain, 'No Encryption']
 
251
 
 
252
 
 
253
 
 
254
 
 
255
 
 
256
#############################################################################
 
257
def initialColResize(tblViewObj, sizeList):
 
258
   """
 
259
   We assume that all percentages are below 1, all fixed >1.  
 
260
   TODO:  This seems to almost work.  providing exactly 100% input will
 
261
          actually result in somewhere between 75% and 125% (approx).  
 
262
          For now, I have to experiment with initial values a few times
 
263
          before getting it to a satisfactory initial size.
 
264
   """   
 
265
   totalWidth = tblViewObj.width()
 
266
   fixedCols, pctCols = [],[]
 
267
 
 
268
   nCols = tblViewObj.model().columnCount()
 
269
   
 
270
   for col,colVal in enumerate(sizeList):
 
271
      if colVal > 1:
 
272
         fixedCols.append( (col, colVal) )
 
273
      else:
 
274
         pctCols.append( (col, colVal) )
 
275
 
 
276
   for c,sz in fixedCols:
 
277
      tblViewObj.horizontalHeader().resizeSection(c, sz)
 
278
 
 
279
   totalFixed = sum([sz[1] for sz in fixedCols])
 
280
   szRemain = totalWidth-totalFixed
 
281
   for c,pct in pctCols:
 
282
      tblViewObj.horizontalHeader().resizeSection(c, pct*szRemain)
 
283
 
 
284
   tblViewObj.horizontalHeader().setStretchLastSection(True)
 
285
 
 
286
 
 
287
 
 
288
 
 
289
class QRichLabel(QLabel):
 
290
   def __init__(self, txt, doWrap=True, \
 
291
                           hAlign=Qt.AlignLeft, \
 
292
                           vAlign=Qt.AlignVCenter, \
 
293
                           **kwargs):
 
294
      super(QRichLabel, self).__init__(txt)
 
295
      self.setTextFormat(Qt.RichText)
 
296
      self.setWordWrap(doWrap)
 
297
      self.setAlignment(hAlign | vAlign)
 
298
      self.setText(txt, **kwargs)
 
299
      # Fixes a problem with QLabel resizing based on content
 
300
      # ACR:  ... and makes other problems.  Removing for now.
 
301
      #self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.MinimumExpanding)
 
302
      #self.setMinimumHeight(int(relaxedSizeStr(self, 'QWERTYqypgj')[1]))
 
303
 
 
304
   def setText(self, text, color=None, size=None, bold=None, italic=None):
 
305
      text = unicode(text)
 
306
      if color:
 
307
         text = '<font color="%s">%s</font>' % (htmlColor(color), text)
 
308
      if size:
 
309
         if isinstance(size, int):
 
310
            text = '<font size=%d>%s</font>' % (size, text)
 
311
         else:
 
312
            text = '<font size="%s">%s</font>' % (size, text)
 
313
      if bold:
 
314
         text = '<b>%s</b>' % text
 
315
      if italic:
 
316
         text = '<i>%s</i>' % text
 
317
 
 
318
      super(QRichLabel, self).setText(text)
 
319
 
 
320
   def setBold(self):
 
321
      self.setText('<b>' + self.text() + '</b>')
 
322
      
 
323
   def setItalic(self):
 
324
      self.setText('<i>' + self.text() + '</i>')
 
325
 
 
326
 
 
327
 
 
328
class QMoneyLabel(QRichLabel):
 
329
   def __init__(self, nSatoshi, ndec=8, maxZeros=2, wColor=True, 
 
330
                              wBold=False, txtSize=10):
 
331
      QLabel.__init__(self, coin2str(nSatoshi))
 
332
 
 
333
      self.setValueText(nSatoshi, ndec, maxZeros, wColor, wBold, txtSize)
 
334
 
 
335
 
 
336
   def setValueText(self, nSatoshi, ndec=None, maxZeros=None, wColor=None, 
 
337
                                             wBold=None, txtSize=10):
 
338
      """
 
339
      When we set the text of the QMoneyLabel, remember previous values unless
 
340
      explicitly respecified
 
341
      """
 
342
      if not ndec is None:
 
343
         self.ndec = ndec
 
344
 
 
345
      if not maxZeros is None:
 
346
         self.max0 = maxZeros
 
347
 
 
348
      if not wColor is None:
 
349
         self.colr = wColor
 
350
 
 
351
      if not wBold is None:
 
352
         self.bold = wBold
 
353
         
 
354
 
 
355
      theFont = GETFONT("Fixed", txtSize)
 
356
      if self.bold:
 
357
         theFont.setWeight(QFont.Bold)
 
358
 
 
359
      self.setFont(theFont)
 
360
      self.setWordWrap(False)
 
361
      valStr = coin2str(nSatoshi, ndec=self.ndec, maxZeros=self.max0)
 
362
      goodMoney = htmlColor('MoneyPos')
 
363
      badMoney  = htmlColor('MoneyNeg')
 
364
      if nSatoshi < 0 and self.colr:
 
365
         self.setText('<font color=%s>%s</font>' % (badMoney, valStr))
 
366
      elif nSatoshi > 0 and self.colr:
 
367
         self.setText('<font color=%s>%s</font>' % (goodMoney, valStr))
 
368
      else:
 
369
         self.setText('%s' % valStr)
 
370
      self.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
 
371
 
 
372
 
 
373
def setLayoutStretchRows(layout, *args):
 
374
   for i,st in enumerate(args):
 
375
      layout.setRowStretch(i, st)
 
376
 
 
377
def setLayoutStretchCols(layout, *args):
 
378
   for i,st in enumerate(args):
 
379
      layout.setColumnStretch(i, st)
 
380
 
 
381
# Use this for QHBoxLayout and QVBoxLayout, where you don't specify dimension
 
382
def setLayoutStretch(layout, *args):
 
383
   for i,st in enumerate(args):
 
384
      layout.setStretch(i, st)
 
385
 
 
386
################################################################################
 
387
def QPixmapButton(img):
 
388
   btn = QPushButton('')
 
389
   px = QPixmap(img)
 
390
   btn.setIcon( QIcon(px))
 
391
   btn.setIconSize(px.rect().size())
 
392
   return btn
 
393
################################################################################
 
394
def QAcceptButton():
 
395
   return QPixmapButton('img/btnaccept.png')
 
396
def QCancelButton():
 
397
   return QPixmapButton('img/btncancel.png')
 
398
def QBackButton():
 
399
   return QPixmapButton('img/btnback.png')
 
400
def QOkButton():
 
401
   return QPixmapButton('img/btnok.png')
 
402
def QDoneButton():
 
403
   return QPixmapButton('img/btndone.png')
 
404
   
 
405
 
 
406
################################################################################
 
407
class QLabelButton(QLabel):
 
408
   mousePressOn = set()
 
409
 
 
410
   def __init__(self, txt):
 
411
      colorStr = htmlColor('LBtnNormalFG')
 
412
      QLabel.__init__(self, '<font color=%s>%s</u></font>' % (colorStr, txt))
 
413
      self.plainText = txt
 
414
      self.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
 
415
 
 
416
  
 
417
   def sizeHint(self):
 
418
      w,h = relaxedSizeStr(self, self.plainText)
 
419
      return QSize(w,1.2*h)
 
420
 
 
421
   def mousePressEvent(self, ev):  
 
422
      # Prevent click-bleed-through to dialogs being opened
 
423
      txt = toBytes(unicode(self.text()))
 
424
      self.mousePressOn.add(txt)
 
425
 
 
426
   def mouseReleaseEvent(self, ev):  
 
427
      txt = toBytes(unicode(self.text()))
 
428
      if txt in self.mousePressOn:
 
429
         self.mousePressOn.remove(txt)
 
430
         self.emit(SIGNAL('clicked()'))  
 
431
 
 
432
   def enterEvent(self, ev):  
 
433
      ssStr = "QLabel { background-color : %s }" % htmlColor('LBtnHoverBG')
 
434
      self.setStyleSheet(ssStr)
 
435
 
 
436
   def leaveEvent(self, ev):
 
437
      ssStr = "QLabel { background-color : %s }" % htmlColor('LBtnNormalBG')
 
438
      self.setStyleSheet(ssStr)
 
439
 
 
440
################################################################################
 
441
# The optionalMsg argument is not word wrapped so the caller is responsible for limiting
 
442
# the length of the longest line in the optionalMsg
 
443
def MsgBoxCustom(wtype, title, msg, wCancel=False, yesStr=None, noStr=None, 
 
444
                                                      optionalMsg=None): 
 
445
   """
 
446
   Creates a message box with custom button text and icon
 
447
   """
 
448
 
 
449
   class dlgWarn(ArmoryDialog):
 
450
      def __init__(self, dtype, dtitle, wmsg, withCancel=False, yesStr=None, noStr=None):
 
451
         super(dlgWarn, self).__init__(None)
 
452
         
 
453
         msgIcon = QLabel()
 
454
         fpix = ''
 
455
         if dtype==MSGBOX.Good:
 
456
            fpix = ':/MsgBox_good48.png'
 
457
         if dtype==MSGBOX.Info:
 
458
            fpix = ':/MsgBox_info48.png'
 
459
         if dtype==MSGBOX.Question:
 
460
            fpix = ':/MsgBox_question64.png'
 
461
         if dtype==MSGBOX.Warning:
 
462
            fpix = ':/MsgBox_warning48.png'
 
463
         if dtype==MSGBOX.Critical:
 
464
            fpix = ':/MsgBox_critical64.png'
 
465
         if dtype==MSGBOX.Error:
 
466
            fpix = ':/MsgBox_error64.png'
 
467
   
 
468
   
 
469
         if len(fpix)>0:
 
470
            msgIcon.setPixmap(QPixmap(fpix))
 
471
            msgIcon.setAlignment(Qt.AlignHCenter | Qt.AlignTop)
 
472
   
 
473
         lblMsg = QLabel(msg)
 
474
         lblMsg.setTextFormat(Qt.RichText)
 
475
         lblMsg.setWordWrap(True)
 
476
         lblMsg.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
 
477
         lblMsg.setOpenExternalLinks(True)
 
478
         w,h = tightSizeNChar(lblMsg, 70)
 
479
         lblMsg.setMinimumSize( w, 3.2*h )
 
480
         buttonbox = QDialogButtonBox()
 
481
 
 
482
         if dtype==MSGBOX.Question:
 
483
            if not yesStr: yesStr = '&Yes'
 
484
            if not noStr:  noStr = '&No'
 
485
            btnYes = QPushButton(yesStr)
 
486
            btnNo  = QPushButton(noStr)
 
487
            self.connect(btnYes, SIGNAL('clicked()'), self.accept)
 
488
            self.connect(btnNo,  SIGNAL('clicked()'), self.reject)
 
489
            buttonbox.addButton(btnYes,QDialogButtonBox.AcceptRole)
 
490
            buttonbox.addButton(btnNo, QDialogButtonBox.RejectRole)
 
491
         else:
 
492
            cancelStr = '&Cancel' if (noStr is not None or withCancel) else ''
 
493
            yesStr    = '&OK' if (yesStr is None) else yesStr
 
494
            btnOk     = QPushButton(yesStr)
 
495
            btnCancel = QPushButton(cancelStr)
 
496
            self.connect(btnOk,     SIGNAL('clicked()'), self.accept)
 
497
            self.connect(btnCancel, SIGNAL('clicked()'), self.reject)
 
498
            buttonbox.addButton(btnOk, QDialogButtonBox.AcceptRole)
 
499
            if cancelStr:
 
500
               buttonbox.addButton(btnCancel, QDialogButtonBox.RejectRole)
 
501
 
 
502
         spacer = QSpacerItem(20, 10, QSizePolicy.Fixed, QSizePolicy.Expanding)
 
503
 
 
504
         layout = QGridLayout()
 
505
         layout.addItem(  spacer,         0,0, 1,2)
 
506
         layout.addWidget(msgIcon,        1,0, 1,1)
 
507
         layout.addWidget(lblMsg,         1,1, 1,1)
 
508
         if optionalMsg:
 
509
            optionalTextLabel = QLabel(optionalMsg)
 
510
            optionalTextLabel.setTextFormat(Qt.RichText)
 
511
            optionalTextLabel.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
 
512
            w,h = tightSizeNChar(optionalTextLabel, 70)
 
513
            optionalTextLabel.setMinimumSize( w, 3.2*h )
 
514
            layout.addWidget(optionalTextLabel, 2,0,1,2)
 
515
         layout.addWidget(buttonbox, 3,0, 1,2)
 
516
         layout.setSpacing(20)
 
517
         self.setLayout(layout)
 
518
         self.setWindowTitle(dtitle)
 
519
 
 
520
   dlg = dlgWarn(wtype, title, msg, wCancel, yesStr, noStr) 
 
521
   result = dlg.exec_()
 
522
   
 
523
   return result
 
524
 
 
525
 
 
526
################################################################################
 
527
def MsgBoxWithDNAA(wtype, title, msg, dnaaMsg, wCancel=False, \
 
528
                   yesStr='Yes', noStr='No', dnaaStartChk=False):
 
529
   """
 
530
   Creates a warning/question/critical dialog, but with a "Do not ask again"
 
531
   checkbox.  Will return a pair  (response, DNAA-is-checked)
 
532
   """
 
533
 
 
534
   class dlgWarn(ArmoryDialog):
 
535
      def __init__(self, dtype, dtitle, wmsg, dmsg=None, withCancel=False): 
 
536
         super(dlgWarn, self).__init__(None)
 
537
         
 
538
         msgIcon = QLabel()
 
539
         fpix = ''
 
540
         if dtype==MSGBOX.Info:
 
541
            fpix = ':/MsgBox_info48.png'
 
542
            if not dmsg:  dmsg = 'Do not show this message again'
 
543
         if dtype==MSGBOX.Question:
 
544
            fpix = ':/MsgBox_question64.png'
 
545
            if not dmsg:  dmsg = 'Do not ask again'
 
546
         if dtype==MSGBOX.Warning:
 
547
            fpix = ':/MsgBox_warning48.png'
 
548
            if not dmsg:  dmsg = 'Do not show this warning again'
 
549
         if dtype==MSGBOX.Critical:
 
550
            fpix = ':/MsgBox_critical64.png'
 
551
            if not dmsg:  dmsg = None  # should always show crits
 
552
         if dtype==MSGBOX.Error:
 
553
            fpix = ':/MsgBox_error64.png'
 
554
            if not dmsg:  dmsg = None  # should always show errors
 
555
   
 
556
   
 
557
         if len(fpix)>0:
 
558
            msgIcon.setPixmap(QPixmap(fpix))
 
559
            msgIcon.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
 
560
   
 
561
         self.chkDnaa = QCheckBox(dmsg)
 
562
         self.chkDnaa.setChecked(dnaaStartChk)
 
563
         lblMsg = QLabel(msg)
 
564
         lblMsg.setTextFormat(Qt.RichText)
 
565
         lblMsg.setWordWrap(True)
 
566
         lblMsg.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
 
567
         w,h = tightSizeNChar(lblMsg, 50)
 
568
         lblMsg.setMinimumSize( w, 3.2*h )
 
569
         lblMsg.setOpenExternalLinks(True)
 
570
 
 
571
         buttonbox = QDialogButtonBox()
 
572
 
 
573
         if dtype==MSGBOX.Question:
 
574
            btnYes = QPushButton(yesStr)
 
575
            btnNo  = QPushButton(noStr)
 
576
            self.connect(btnYes, SIGNAL('clicked()'), self.accept)
 
577
            self.connect(btnNo,  SIGNAL('clicked()'), self.reject)
 
578
            buttonbox.addButton(btnYes,QDialogButtonBox.AcceptRole)
 
579
            buttonbox.addButton(btnNo, QDialogButtonBox.RejectRole)
 
580
         else:
 
581
            btnOk = QPushButton('Ok')
 
582
            self.connect(btnOk, SIGNAL('clicked()'), self.accept)
 
583
            buttonbox.addButton(btnOk, QDialogButtonBox.AcceptRole)
 
584
            if withCancel:
 
585
               btnOk = QPushButton('Cancel')
 
586
               self.connect(btnOk, SIGNAL('clicked()'), self.reject)
 
587
               buttonbox.addButton(btnOk, QDialogButtonBox.RejectRole)
 
588
            
 
589
 
 
590
         spacer = QSpacerItem(20, 10, QSizePolicy.Fixed, QSizePolicy.Expanding)
 
591
 
 
592
 
 
593
         layout = QGridLayout()
 
594
         layout.addItem(  spacer,         0,0, 1,2)
 
595
         layout.addWidget(msgIcon,        1,0, 1,1)
 
596
         layout.addWidget(lblMsg,         1,1, 1,1)
 
597
         layout.addWidget(self.chkDnaa,   2,0, 1,2)
 
598
         layout.addWidget(buttonbox,      3,0, 1,2)
 
599
         layout.setSpacing(20)
 
600
         self.setLayout(layout)
 
601
         self.setWindowTitle(dtitle)
 
602
 
 
603
 
 
604
   dlg = dlgWarn(wtype, title, msg, dnaaMsg, wCancel) 
 
605
   result = dlg.exec_()
 
606
   
 
607
   return (result, dlg.chkDnaa.isChecked())
 
608
 
 
609
 
 
610
def makeLayoutFrame(dirStr, widgetList, style=QFrame.NoFrame, condenseMargins=False):
 
611
   frm = QFrame()
 
612
   frm.setFrameStyle(style)
 
613
 
 
614
   frmLayout = QHBoxLayout()
 
615
   if dirStr.lower().startswith(VERTICAL):
 
616
      frmLayout = QVBoxLayout()
 
617
      
 
618
   for w in widgetList:
 
619
      if isinstance(w,str) and w.lower()=='stretch':
 
620
         frmLayout.addStretch()
 
621
      elif isinstance(w,str) and w.lower().startswith('space'):
 
622
         # expect "spacer(30)"
 
623
         first = w.index('(')+1 
 
624
         last  = w.index(')')
 
625
         wid,hgt = int(w[first:last]), 1
 
626
         if dirStr.lower().startswith(VERTICAL):
 
627
            wid,hgt = hgt,wid
 
628
         frmLayout.addItem( QSpacerItem(wid,hgt) )
 
629
      elif isinstance(w,str) and w.lower().startswith('line'):
 
630
         frmLine = QFrame()
 
631
         if dirStr.lower().startswith(VERTICAL):
 
632
            frmLine.setFrameStyle(QFrame.HLine | QFrame.Plain)
 
633
         else:
 
634
            frmLine.setFrameStyle(QFrame.VLine | QFrame.Plain)
 
635
         frmLayout.addWidget(frmLine)
 
636
      elif isinstance(w,str) and w.lower().startswith('strut'):
 
637
         first = w.index('(')+1 
 
638
         last  = w.index(')')
 
639
         strutSz = int(w[first:last])
 
640
         frmLayout.addStrut(strutSz)
 
641
      elif isinstance(w,QSpacerItem):
 
642
         frmLayout.addItem(w)
 
643
      else:
 
644
         frmLayout.addWidget(w)
 
645
 
 
646
   if condenseMargins:
 
647
      frmLayout.setContentsMargins(3,3,3,3)
 
648
      frmLayout.setSpacing(3)
 
649
   else:
 
650
      frmLayout.setContentsMargins(5,5,5,5)
 
651
   frm.setLayout(frmLayout)
 
652
   return frm
 
653
   
 
654
 
 
655
def addFrame(widget, style=STYLE_SUNKEN, condenseMargins=False):
 
656
   return makeLayoutFrame(HORIZONTAL, [widget], style, condenseMargins)
 
657
   
 
658
def makeVertFrame(widgetList, style=QFrame.NoFrame, condenseMargins=False):
 
659
   return makeLayoutFrame(VERTICAL, widgetList, style, condenseMargins)
 
660
 
 
661
def makeHorizFrame(widgetList, style=QFrame.NoFrame, condenseMargins=False):
 
662
   return makeLayoutFrame(HORIZONTAL, widgetList, style, condenseMargins)
 
663
 
 
664
 
 
665
def QImageLabel(imgfn, size=None, stretch='NoStretch'):
 
666
 
 
667
   lbl = QLabel()
 
668
 
 
669
   if size==None:
 
670
      px = QPixmap(imgfn)
 
671
   else:
 
672
      px = QPixmap(imgfn).scaled(*size)  # expect size=(W,H)
 
673
 
 
674
   lbl.setPixmap(px)
 
675
   return lbl
 
676
   
 
677
 
 
678
 
 
679
 
 
680
def restoreTableView(qtbl, hexBytes):
 
681
   try:
 
682
      binunpack = BinaryUnpacker(hex_to_binary(hexBytes))
 
683
      hexByte = binunpack.get(UINT8)
 
684
      binLen = binunpack.get(UINT8)
 
685
      toRestore = []
 
686
      for i in range(binLen):
 
687
         sz = binunpack.get(UINT16)
 
688
         if sz>0:
 
689
            toRestore.append([i,sz])
 
690
         
 
691
      for i,c in toRestore[:-1]:
 
692
         qtbl.setColumnWidth(i, c)
 
693
   except Exception, e:
 
694
      print 'ERROR!'
 
695
      pass
 
696
      # Don't want to crash the program just because couldn't load tbl data
 
697
 
 
698
 
 
699
def saveTableView(qtbl):
 
700
   nCol = qtbl.model().columnCount()
 
701
   sz = [None]*nCol
 
702
   for i in range(nCol):
 
703
      sz[i] = qtbl.columnWidth(i)      
 
704
 
 
705
   # Use 'ff' as a kind of magic byte for this data.  Most importantly
 
706
   # we want to guarantee that the settings file will interpret this
 
707
   # as hex data -- I once had an unlucky hex string written out with 
 
708
   # all digits and then intepretted as an integer on the next load :( 
 
709
   first = int_to_hex(nCol)
 
710
   rest  = [int_to_hex(s, widthBytes=2) for s in sz]
 
711
   return 'ff' + first + ''.join(rest)
 
712
 
 
713
 
 
714
 
 
715
 
 
716
 
 
717
################################################################################
 
718
# This class is intended to be an abstract frame class that
 
719
# will hold all of the functionality that is common to all 
 
720
# Frames used in Armory. 
 
721
# The Frames that extend this class should contain all of the
 
722
# display and control components for some screen used in Armory
 
723
# Putting this content in a frame allows it to be used on it's own
 
724
# in a dialog or as a component in a larger frame.
 
725
class ArmoryFrame(QFrame):
 
726
   def __init__(self, parent, main):
 
727
      super(ArmoryFrame, self).__init__(parent)
 
728
      self.main = main
 
729
 
 
730
      # Subclasses should implement a method that returns a boolean to control
 
731
      # when done, accept, next, or final button should be enabled.
 
732
      self.isComplete = None
 
733
 
 
734
 
 
735
################################################################################
 
736
class ArmoryDialog(QDialog):
 
737
   def __init__(self, parent=None, main=None):
 
738
      super(ArmoryDialog, self).__init__(parent)
 
739
 
 
740
      self.parent = parent
 
741
      self.main   = main
 
742
 
 
743
      self.setFont(GETFONT('var'))
 
744
      self.setWindowFlags(Qt.Window)
 
745
 
 
746
      if USE_TESTNET:
 
747
         self.setWindowTitle('Armory - Bitcoin Wallet Management [TESTNET]')
 
748
         self.setWindowIcon(QIcon(':/armory_icon_green_32x32.png'))
 
749
      else:
 
750
         self.setWindowTitle('Armory - Bitcoin Wallet Management')
 
751
         self.setWindowIcon(QIcon(':/armory_icon_32x32.png'))
 
752
   
 
753
   @AddToRunningDialogsList
 
754
   def exec_(self):
 
755
      return super(ArmoryDialog, self).exec_()
 
756
      
 
757
 
 
758
################################################################################
 
759
class QRCodeWidget(QWidget):
 
760
 
 
761
   def __init__(self, asciiToEncode='', prefSize=160, errLevel='L', parent=None):
 
762
      super(QRCodeWidget, self).__init__()
 
763
 
 
764
      self.parent = parent
 
765
      self.qrmtrx = None
 
766
      self.setAsciiData(asciiToEncode, prefSize, errLevel, repaint=False)
 
767
      
 
768
 
 
769
   def setAsciiData(self, newAscii, prefSize=160, errLevel='L', repaint=True):
 
770
      if len(newAscii)==0:
 
771
         self.qrmtrx = [[0]]
 
772
         self.modCt  = 1
 
773
         self.pxScale= 1
 
774
         return
 
775
 
 
776
      self.theData = newAscii
 
777
      self.qrmtrx, self.modCt = CreateQRMatrix(self.theData, errLevel)
 
778
      self.setPreferredSize(prefSize)
 
779
 
 
780
 
 
781
      
 
782
            
 
783
   def getModuleCount1D(self):
 
784
      return self.modCt
 
785
 
 
786
 
 
787
   def setPreferredSize(self, px, policy='Approx'):
 
788
      self.pxScale,rem = divmod(int(px), int(self.modCt))
 
789
 
 
790
      if policy.lower().startswith('approx'):
 
791
         if rem>self.modCt/2.0:
 
792
            self.pxScale += 1
 
793
      elif policy.lower().startswith('atleast'):
 
794
         if rem>0:
 
795
            self.pxScale += 1
 
796
      elif policy.lower().startswith('max'):
 
797
         pass
 
798
      else:
 
799
         LOGERROR('Bad size policy in set qr size')
 
800
         return self.pxScale*self.modCt
 
801
 
 
802
      return
 
803
      
 
804
 
 
805
   def getSize(self):
 
806
      return self.pxScale*self.modCt
 
807
 
 
808
       
 
809
   def sizeHint(self):
 
810
      sz1d = self.pxScale*self.modCt
 
811
      return QSize(sz1d, sz1d)
 
812
 
 
813
 
 
814
   def paintEvent(self, e):
 
815
      qp = QPainter()
 
816
      qp.begin(self)
 
817
      self.drawWidget(qp)
 
818
      qp.end()
 
819
 
 
820
 
 
821
 
 
822
   def drawWidget(self, qp):
 
823
      # In case this is not a white background, draw the white boxes
 
824
      qp.setPen(QColor(255,255,255))
 
825
      qp.setBrush(QColor(255,255,255))
 
826
      for r in range(self.modCt):
 
827
         for c in range(self.modCt):
 
828
            if not self.qrmtrx[r][c]:
 
829
               qp.drawRect(*[a*self.pxScale for a in [r,c,1,1]])
 
830
 
 
831
      # Draw the black tiles
 
832
      qp.setPen(QColor(0,0,0))
 
833
      qp.setBrush(QColor(0,0,0))
 
834
      for r in range(self.modCt):
 
835
         for c in range(self.modCt):
 
836
            if self.qrmtrx[r][c]:
 
837
               qp.drawRect(*[a*self.pxScale for a in [r,c,1,1]])
 
838
 
 
839
 
 
840
   def mouseDoubleClickEvent(self, *args):
 
841
      DlgInflatedQR(self.parent, self.theData).exec_()
 
842
            
 
843
            
 
844
# Create a very simple dialog and execute it
 
845
class DlgInflatedQR(ArmoryDialog):
 
846
   def __init__(self, parent, dataToQR):
 
847
      super(DlgInflatedQR, self).__init__(parent)
 
848
 
 
849
      sz = QApplication.desktop().size()
 
850
      w,h = sz.width(), sz.height()
 
851
      qrSize = int(min(w,h)*0.8)
 
852
      qrDisp = QRCodeWidget(dataToQR, prefSize=qrSize)
 
853
 
 
854
      def closeDlg(*args): 
 
855
         self.accept()
 
856
      qrDisp.mouseDoubleClickEvent = closeDlg
 
857
      self.mouseDoubleClickEvent = closeDlg
 
858
 
 
859
      lbl = QRichLabel('<b>Double-click or press ESC to close</b>')
 
860
      lbl.setAlignment(Qt.AlignTop | Qt.AlignHCenter)
 
861
 
 
862
      frmQR = makeHorizFrame(['Stretch', qrDisp, 'Stretch'])
 
863
      frmFull = makeVertFrame(['Stretch',frmQR, lbl, 'Stretch'])
 
864
 
 
865
      layout = QVBoxLayout()
 
866
      layout.addWidget(frmFull)
 
867
 
 
868
      self.setLayout(layout)
 
869
      self.showFullScreen()
 
870
      
 
871
 
 
872
 
 
873
 
 
874
 
 
875
 
 
876
# Pure-python BMP creator taken from:
 
877
#
 
878
#     http://pseentertainmentcorp.com/smf/index.php?topic=2034.0
 
879
#
 
880
# This will take a 2D array of ones-and-zeros and convert it to a binary
 
881
# bitmap image, which will be stored in a temporary file.  This temporary
 
882
# file can be used for display and copy-and-paste into email.
 
883
 
 
884
def bmp_binary(header, pixels):
 
885
   '''It takes a header (based on default_bmp_header), 
 
886
   the pixel data (from structs, as produced by get_color and row_padding),
 
887
   and writes it to filename'''
 
888
   header_str = ""
 
889
   header_str += struct.pack('<B', header['mn1'])
 
890
   header_str += struct.pack('<B', header['mn2'])
 
891
   header_str += struct.pack('<L', header['filesize'])
 
892
   header_str += struct.pack('<H', header['undef1'])
 
893
   header_str += struct.pack('<H', header['undef2'])
 
894
   header_str += struct.pack('<L', header['offset'])
 
895
   header_str += struct.pack('<L', header['headerlength'])
 
896
   header_str += struct.pack('<L', header['width'])
 
897
   header_str += struct.pack('<L', header['height'])
 
898
   header_str += struct.pack('<H', header['colorplanes'])
 
899
   header_str += struct.pack('<H', header['colordepth'])
 
900
   header_str += struct.pack('<L', header['compression'])
 
901
   header_str += struct.pack('<L', header['imagesize'])
 
902
   header_str += struct.pack('<L', header['res_hor'])
 
903
   header_str += struct.pack('<L', header['res_vert'])
 
904
   header_str += struct.pack('<L', header['palette'])
 
905
   header_str += struct.pack('<L', header['importantcolors'])
 
906
   return header_str + pixels
 
907
 
 
908
def bmp_write(header, pixels, filename):
 
909
   out = open(filename, 'wb')
 
910
   out.write(bmp_binary(header, pixels))
 
911
   out.close()
 
912
 
 
913
def bmp_row_padding(width, colordepth):
 
914
   '''returns any necessary row padding'''
 
915
   byte_length = width*colordepth/8
 
916
   # how many bytes are needed to make byte_length evenly divisible by 4?
 
917
   padding = (4-byte_length)%4 
 
918
   padbytes = ''
 
919
   for i in range(padding):
 
920
      x = struct.pack('<B',0)
 
921
      padbytes += x
 
922
   return padbytes
 
923
 
 
924
def bmp_pack_color(red, green, blue):
 
925
   '''accepts values from 0-255 for each value, returns a packed string'''
 
926
   return struct.pack('<BBB',blue,green,red)
 
927
 
 
928
 
 
929
###################################   
 
930
BMP_TEMPFILE = -1
 
931
def createBitmap(imgMtrx2D, writeToFile=-1, returnBinary=True):
 
932
   try:
 
933
      h,w = len(imgMtrx2D), len(imgMtrx2D[0])
 
934
   except:
 
935
      LOGERROR('Error creating BMP object')
 
936
      raise
 
937
 
 
938
   header = {'mn1':66,
 
939
             'mn2':77,
 
940
             'filesize':0,
 
941
             'undef1':0,
 
942
             'undef2':0,
 
943
             'offset':54,
 
944
             'headerlength':40,
 
945
             'width':w,
 
946
             'height':h,
 
947
             'colorplanes':0,
 
948
             'colordepth':24,
 
949
             'compression':0,
 
950
             'imagesize':0,
 
951
             'res_hor':0,
 
952
             'res_vert':0,
 
953
             'palette':0,
 
954
             'importantcolors':0}
 
955
 
 
956
   pixels = ''
 
957
   black = bmp_pack_color(  0,  0,  0)
 
958
   white = bmp_pack_color(255,255,255)
 
959
   for row in range(header['height']-1,-1,-1):# (BMPs are L to R from the bottom L row)
 
960
      for col in range(header['width']):
 
961
         pixels += black if imgMtrx2D[row][col] else white
 
962
      pixels += bmp_row_padding(header['width'], header['colordepth'])
 
963
      
 
964
   if returnBinary:
 
965
      return bmp_binary(header,pixels)
 
966
   elif writeToFile==BMP_TEMPFILE:
 
967
      handle,temppath = mkstemp(suffix='.bmp')
 
968
      bmp_write(header, pixels, temppath)
 
969
      return temppath
 
970
   else:
 
971
      try:
 
972
         bmp_write(header, pixels, writeToFile)
 
973
         return True
 
974
      except:
 
975
         return False
 
976
      
 
977
 
 
978
 
 
979
def selectFileForQLineEdit(parent, qObj, title="Select File", existing=False, \
 
980
                           ffilter=[]):
 
981
 
 
982
   types = list(ffilter)
 
983
   types.append('All files (*)')
 
984
   typesStr = ';; '.join(types)
 
985
   if not OS_MACOSX:
 
986
      fullPath = unicode(QFileDialog.getOpenFileName(parent, \
 
987
         title, ARMORY_HOME_DIR, typesStr))
 
988
   else:
 
989
      fullPath = unicode(QFileDialog.getOpenFileName(parent, \
 
990
         title, ARMORY_HOME_DIR, typesStr, options=QFileDialog.DontUseNativeDialog))
 
991
 
 
992
   if fullPath:
 
993
      qObj.setText( fullPath)
 
994
   
 
995
 
 
996
def selectDirectoryForQLineEdit(par, qObj, title="Select Directory"):
 
997
   initPath = ARMORY_HOME_DIR
 
998
   currText = unicode(qObj.text()).strip()
 
999
   if len(currText)>0:
 
1000
      if os.path.exists(currText):
 
1001
         initPath = currText
 
1002
    
 
1003
   if not OS_MACOSX:
 
1004
      fullPath = unicode(QFileDialog.getExistingDirectory(par, title, initPath))
 
1005
   else:
 
1006
      fullPath = unicode(QFileDialog.getExistingDirectory(par, title, initPath, \
 
1007
                                       options=QFileDialog.DontUseNativeDialog))
 
1008
   if fullPath:
 
1009
      qObj.setText( fullPath)
 
1010
    
 
1011
 
 
1012
def createDirectorySelectButton(parent, targetWidget, title="Select Directory"):
 
1013
 
 
1014
   btn = QPushButton('')
 
1015
   ico = QIcon(QPixmap(':/folder24.png')) 
 
1016
   btn.setIcon(ico)
 
1017
 
 
1018
 
 
1019
   fn = lambda: selectDirectoryForQLineEdit(parent, targetWidget, title)
 
1020
   parent.connect(btn, SIGNAL('clicked()'), fn)
 
1021
   return btn
 
1022
 
 
1023
 
 
1024