1
################################################################################
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 #
7
################################################################################
9
from tempfile import mkstemp
11
from PyQt4.QtCore import *
12
from PyQt4.QtGui import *
15
from armorycolors import Colors, htmlColor
16
from armoryengine.ArmoryUtils import *
17
from armoryengine.BinaryUnpacker import *
18
from armoryengine.MultiSigUtils import *
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')
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
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'
49
HTTP_ANNOUNCE_FILE = 'https://bitcoinarmory.com/atiannounce.txt'
51
# Keep track of dialogs and wizard that are executing
52
runningDialogsList = []
54
def AddToRunningDialogsList(func):
55
def wrapper(*args, **kwargs):
56
runningDialogsList.append(args[0])
57
result = func(*args, **kwargs)
58
runningDialogsList.remove(args[0])
62
################################################################################
63
def tr(txt, replList=None, pluralList=None):
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.
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:
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.')
78
Instead it should really look like:
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
86
Added easy plural handling:
88
Just add an extra argument to specify a variable on which plurality
89
should be chosen, and then decorate your text with
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)
99
This should work well for
103
lines = [l.strip() for l in txt.split('\n')]
104
txt = (' '.join(lines)).strip()
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
112
return formatWithPlurals(txt, replList, pluralList)
116
################################################################################
117
def HLINE(style=QFrame.Plain):
119
qf.setFrameStyle(QFrame.HLine | style)
122
def VLINE(style=QFrame.Plain):
124
qf.setFrameStyle(QFrame.VLine | style)
129
# Setup fixed-width and var-width fonts
130
def GETFONT(ftype, sz=10, bold=False, italic=False):
132
if ftype.lower().startswith('fix'):
134
fnt = QFont("Courier", sz)
136
fnt = QFont("Menlo", sz)
138
fnt = QFont("DejaVu Sans Mono", sz)
139
elif ftype.lower().startswith('var'):
141
fnt = QFont("Lucida Grande", sz)
143
fnt = QFont("Verdana", sz)
145
#fnt = QFont("Tahoma", sz)
147
#fnt = QFont("Sans", sz)
148
elif ftype.lower().startswith('money'):
150
fnt = QFont("Courier", sz)
152
fnt = QFont("Menlo", sz)
154
fnt = QFont("DejaVu Sans Mono", sz)
156
fnt = QFont(ftype, sz)
159
fnt.setWeight(QFont.Bold)
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)
179
def UserModeStr(mode):
180
if mode==USERMODE.Standard:
182
elif mode==USERMODE.Advanced:
184
elif mode==USERMODE.Expert:
189
def tightSizeNChar(obj, nChar):
191
Approximates the size of a row text of mixed characters
193
This is only aproximate, since variable-width fonts will vary
194
depending on the specific text
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
206
def tightSizeStr(obj, theStr):
207
""" Measure a specific string """
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
216
def relaxedSizeStr(obj, theStr):
218
Approximates the size of a row text, nchars long, adds some margin
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
228
def relaxedSizeNChar(obj, nChar):
230
Approximates the size of a row text, nchars long, adds some margin
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
240
#############################################################################
241
def determineWalletType(wlt, wndw):
243
if wndw.getWltSetting(wlt.uniqueIDB58, 'IsMine'):
244
return [WLTTYPES.Offline, 'Offline']
246
return [WLTTYPES.WatchOnly, 'Watching-Only']
247
elif wlt.useEncryption:
248
return [WLTTYPES.Crypt, 'Encrypted']
250
return [WLTTYPES.Plain, 'No Encryption']
256
#############################################################################
257
def initialColResize(tblViewObj, sizeList):
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.
265
totalWidth = tblViewObj.width()
266
fixedCols, pctCols = [],[]
268
nCols = tblViewObj.model().columnCount()
270
for col,colVal in enumerate(sizeList):
272
fixedCols.append( (col, colVal) )
274
pctCols.append( (col, colVal) )
276
for c,sz in fixedCols:
277
tblViewObj.horizontalHeader().resizeSection(c, sz)
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)
284
tblViewObj.horizontalHeader().setStretchLastSection(True)
289
class QRichLabel(QLabel):
290
def __init__(self, txt, doWrap=True, \
291
hAlign=Qt.AlignLeft, \
292
vAlign=Qt.AlignVCenter, \
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]))
304
def setText(self, text, color=None, size=None, bold=None, italic=None):
307
text = '<font color="%s">%s</font>' % (htmlColor(color), text)
309
if isinstance(size, int):
310
text = '<font size=%d>%s</font>' % (size, text)
312
text = '<font size="%s">%s</font>' % (size, text)
314
text = '<b>%s</b>' % text
316
text = '<i>%s</i>' % text
318
super(QRichLabel, self).setText(text)
321
self.setText('<b>' + self.text() + '</b>')
324
self.setText('<i>' + self.text() + '</i>')
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))
333
self.setValueText(nSatoshi, ndec, maxZeros, wColor, wBold, txtSize)
336
def setValueText(self, nSatoshi, ndec=None, maxZeros=None, wColor=None,
337
wBold=None, txtSize=10):
339
When we set the text of the QMoneyLabel, remember previous values unless
340
explicitly respecified
345
if not maxZeros is None:
348
if not wColor is None:
351
if not wBold is None:
355
theFont = GETFONT("Fixed", txtSize)
357
theFont.setWeight(QFont.Bold)
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))
369
self.setText('%s' % valStr)
370
self.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
373
def setLayoutStretchRows(layout, *args):
374
for i,st in enumerate(args):
375
layout.setRowStretch(i, st)
377
def setLayoutStretchCols(layout, *args):
378
for i,st in enumerate(args):
379
layout.setColumnStretch(i, st)
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)
386
################################################################################
387
def QPixmapButton(img):
388
btn = QPushButton('')
390
btn.setIcon( QIcon(px))
391
btn.setIconSize(px.rect().size())
393
################################################################################
395
return QPixmapButton('img/btnaccept.png')
397
return QPixmapButton('img/btncancel.png')
399
return QPixmapButton('img/btnback.png')
401
return QPixmapButton('img/btnok.png')
403
return QPixmapButton('img/btndone.png')
406
################################################################################
407
class QLabelButton(QLabel):
410
def __init__(self, txt):
411
colorStr = htmlColor('LBtnNormalFG')
412
QLabel.__init__(self, '<font color=%s>%s</u></font>' % (colorStr, txt))
414
self.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
418
w,h = relaxedSizeStr(self, self.plainText)
419
return QSize(w,1.2*h)
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)
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()'))
432
def enterEvent(self, ev):
433
ssStr = "QLabel { background-color : %s }" % htmlColor('LBtnHoverBG')
434
self.setStyleSheet(ssStr)
436
def leaveEvent(self, ev):
437
ssStr = "QLabel { background-color : %s }" % htmlColor('LBtnNormalBG')
438
self.setStyleSheet(ssStr)
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,
446
Creates a message box with custom button text and icon
449
class dlgWarn(ArmoryDialog):
450
def __init__(self, dtype, dtitle, wmsg, withCancel=False, yesStr=None, noStr=None):
451
super(dlgWarn, self).__init__(None)
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'
470
msgIcon.setPixmap(QPixmap(fpix))
471
msgIcon.setAlignment(Qt.AlignHCenter | Qt.AlignTop)
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()
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)
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)
500
buttonbox.addButton(btnCancel, QDialogButtonBox.RejectRole)
502
spacer = QSpacerItem(20, 10, QSizePolicy.Fixed, QSizePolicy.Expanding)
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)
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)
520
dlg = dlgWarn(wtype, title, msg, wCancel, yesStr, noStr)
526
################################################################################
527
def MsgBoxWithDNAA(wtype, title, msg, dnaaMsg, wCancel=False, \
528
yesStr='Yes', noStr='No', dnaaStartChk=False):
530
Creates a warning/question/critical dialog, but with a "Do not ask again"
531
checkbox. Will return a pair (response, DNAA-is-checked)
534
class dlgWarn(ArmoryDialog):
535
def __init__(self, dtype, dtitle, wmsg, dmsg=None, withCancel=False):
536
super(dlgWarn, self).__init__(None)
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
558
msgIcon.setPixmap(QPixmap(fpix))
559
msgIcon.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
561
self.chkDnaa = QCheckBox(dmsg)
562
self.chkDnaa.setChecked(dnaaStartChk)
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)
571
buttonbox = QDialogButtonBox()
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)
581
btnOk = QPushButton('Ok')
582
self.connect(btnOk, SIGNAL('clicked()'), self.accept)
583
buttonbox.addButton(btnOk, QDialogButtonBox.AcceptRole)
585
btnOk = QPushButton('Cancel')
586
self.connect(btnOk, SIGNAL('clicked()'), self.reject)
587
buttonbox.addButton(btnOk, QDialogButtonBox.RejectRole)
590
spacer = QSpacerItem(20, 10, QSizePolicy.Fixed, QSizePolicy.Expanding)
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)
604
dlg = dlgWarn(wtype, title, msg, dnaaMsg, wCancel)
607
return (result, dlg.chkDnaa.isChecked())
610
def makeLayoutFrame(dirStr, widgetList, style=QFrame.NoFrame, condenseMargins=False):
612
frm.setFrameStyle(style)
614
frmLayout = QHBoxLayout()
615
if dirStr.lower().startswith(VERTICAL):
616
frmLayout = QVBoxLayout()
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
625
wid,hgt = int(w[first:last]), 1
626
if dirStr.lower().startswith(VERTICAL):
628
frmLayout.addItem( QSpacerItem(wid,hgt) )
629
elif isinstance(w,str) and w.lower().startswith('line'):
631
if dirStr.lower().startswith(VERTICAL):
632
frmLine.setFrameStyle(QFrame.HLine | QFrame.Plain)
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
639
strutSz = int(w[first:last])
640
frmLayout.addStrut(strutSz)
641
elif isinstance(w,QSpacerItem):
644
frmLayout.addWidget(w)
647
frmLayout.setContentsMargins(3,3,3,3)
648
frmLayout.setSpacing(3)
650
frmLayout.setContentsMargins(5,5,5,5)
651
frm.setLayout(frmLayout)
655
def addFrame(widget, style=STYLE_SUNKEN, condenseMargins=False):
656
return makeLayoutFrame(HORIZONTAL, [widget], style, condenseMargins)
658
def makeVertFrame(widgetList, style=QFrame.NoFrame, condenseMargins=False):
659
return makeLayoutFrame(VERTICAL, widgetList, style, condenseMargins)
661
def makeHorizFrame(widgetList, style=QFrame.NoFrame, condenseMargins=False):
662
return makeLayoutFrame(HORIZONTAL, widgetList, style, condenseMargins)
665
def QImageLabel(imgfn, size=None, stretch='NoStretch'):
672
px = QPixmap(imgfn).scaled(*size) # expect size=(W,H)
680
def restoreTableView(qtbl, hexBytes):
682
binunpack = BinaryUnpacker(hex_to_binary(hexBytes))
683
hexByte = binunpack.get(UINT8)
684
binLen = binunpack.get(UINT8)
686
for i in range(binLen):
687
sz = binunpack.get(UINT16)
689
toRestore.append([i,sz])
691
for i,c in toRestore[:-1]:
692
qtbl.setColumnWidth(i, c)
696
# Don't want to crash the program just because couldn't load tbl data
699
def saveTableView(qtbl):
700
nCol = qtbl.model().columnCount()
702
for i in range(nCol):
703
sz[i] = qtbl.columnWidth(i)
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)
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)
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
735
################################################################################
736
class ArmoryDialog(QDialog):
737
def __init__(self, parent=None, main=None):
738
super(ArmoryDialog, self).__init__(parent)
743
self.setFont(GETFONT('var'))
744
self.setWindowFlags(Qt.Window)
747
self.setWindowTitle('Armory - Bitcoin Wallet Management [TESTNET]')
748
self.setWindowIcon(QIcon(':/armory_icon_green_32x32.png'))
750
self.setWindowTitle('Armory - Bitcoin Wallet Management')
751
self.setWindowIcon(QIcon(':/armory_icon_32x32.png'))
753
@AddToRunningDialogsList
755
return super(ArmoryDialog, self).exec_()
758
################################################################################
759
class QRCodeWidget(QWidget):
761
def __init__(self, asciiToEncode='', prefSize=160, errLevel='L', parent=None):
762
super(QRCodeWidget, self).__init__()
766
self.setAsciiData(asciiToEncode, prefSize, errLevel, repaint=False)
769
def setAsciiData(self, newAscii, prefSize=160, errLevel='L', repaint=True):
776
self.theData = newAscii
777
self.qrmtrx, self.modCt = CreateQRMatrix(self.theData, errLevel)
778
self.setPreferredSize(prefSize)
783
def getModuleCount1D(self):
787
def setPreferredSize(self, px, policy='Approx'):
788
self.pxScale,rem = divmod(int(px), int(self.modCt))
790
if policy.lower().startswith('approx'):
791
if rem>self.modCt/2.0:
793
elif policy.lower().startswith('atleast'):
796
elif policy.lower().startswith('max'):
799
LOGERROR('Bad size policy in set qr size')
800
return self.pxScale*self.modCt
806
return self.pxScale*self.modCt
810
sz1d = self.pxScale*self.modCt
811
return QSize(sz1d, sz1d)
814
def paintEvent(self, e):
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]])
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]])
840
def mouseDoubleClickEvent(self, *args):
841
DlgInflatedQR(self.parent, self.theData).exec_()
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)
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)
856
qrDisp.mouseDoubleClickEvent = closeDlg
857
self.mouseDoubleClickEvent = closeDlg
859
lbl = QRichLabel('<b>Double-click or press ESC to close</b>')
860
lbl.setAlignment(Qt.AlignTop | Qt.AlignHCenter)
862
frmQR = makeHorizFrame(['Stretch', qrDisp, 'Stretch'])
863
frmFull = makeVertFrame(['Stretch',frmQR, lbl, 'Stretch'])
865
layout = QVBoxLayout()
866
layout.addWidget(frmFull)
868
self.setLayout(layout)
869
self.showFullScreen()
876
# Pure-python BMP creator taken from:
878
# http://pseentertainmentcorp.com/smf/index.php?topic=2034.0
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.
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'''
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
908
def bmp_write(header, pixels, filename):
909
out = open(filename, 'wb')
910
out.write(bmp_binary(header, pixels))
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
919
for i in range(padding):
920
x = struct.pack('<B',0)
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)
929
###################################
931
def createBitmap(imgMtrx2D, writeToFile=-1, returnBinary=True):
933
h,w = len(imgMtrx2D), len(imgMtrx2D[0])
935
LOGERROR('Error creating BMP object')
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'])
965
return bmp_binary(header,pixels)
966
elif writeToFile==BMP_TEMPFILE:
967
handle,temppath = mkstemp(suffix='.bmp')
968
bmp_write(header, pixels, temppath)
972
bmp_write(header, pixels, writeToFile)
979
def selectFileForQLineEdit(parent, qObj, title="Select File", existing=False, \
982
types = list(ffilter)
983
types.append('All files (*)')
984
typesStr = ';; '.join(types)
986
fullPath = unicode(QFileDialog.getOpenFileName(parent, \
987
title, ARMORY_HOME_DIR, typesStr))
989
fullPath = unicode(QFileDialog.getOpenFileName(parent, \
990
title, ARMORY_HOME_DIR, typesStr, options=QFileDialog.DontUseNativeDialog))
993
qObj.setText( fullPath)
996
def selectDirectoryForQLineEdit(par, qObj, title="Select Directory"):
997
initPath = ARMORY_HOME_DIR
998
currText = unicode(qObj.text()).strip()
1000
if os.path.exists(currText):
1004
fullPath = unicode(QFileDialog.getExistingDirectory(par, title, initPath))
1006
fullPath = unicode(QFileDialog.getExistingDirectory(par, title, initPath, \
1007
options=QFileDialog.DontUseNativeDialog))
1009
qObj.setText( fullPath)
1012
def createDirectorySelectButton(parent, targetWidget, title="Select Directory"):
1014
btn = QPushButton('')
1015
ico = QIcon(QPixmap(':/folder24.png'))
1019
fn = lambda: selectDirectoryForQLineEdit(parent, targetWidget, title)
1020
parent.connect(btn, SIGNAL('clicked()'), fn)