source: orange/Orange/OrangeCanvas/orngDlgs.py @ 11695:efdd815552d0

Revision 11695:efdd815552d0, 61.9 KB checked in by Ales Erjavec <ales.erjavec@…>, 7 months ago (diff)

Use 'easy_install' to install add-ons.

Line 
1# Author: Gregor Leban (gregor.leban@fri.uni-lj.si)
2# Description:
3#    signal dialog, canvas options dialog
4
5import sys
6import os
7import subprocess
8from contextlib import closing
9
10from PyQt4.QtCore import *
11from PyQt4.QtGui import *
12from orngCanvasItems import MyCanvasText
13import time
14
15import OWGUI
16import Orange.utils.addons
17
18has_pip = True
19try:
20    import pip.req
21except ImportError:
22    has_pip = False
23
24# this class is needed by signalDialog to show widgets and lines
25class SignalCanvasView(QGraphicsView):
26    def __init__(self, dlg, canvasDlg, *args):
27        apply(QGraphicsView.__init__,(self,) + args)
28        self.dlg = dlg
29        self.canvasDlg = canvasDlg
30        self.bMouseDown = False
31        self.tempLine = None
32        self.inWidget = None
33        self.outWidget = None
34        self.inWidgetIcon = None
35        self.outWidgetIcon = None
36        self.lines = []
37        self.outBoxes = []
38        self.inBoxes = []
39        self.texts = []
40        self.ensureVisible(0,0,1,1)
41        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
42        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
43        self.setAlignment(Qt.AlignLeft | Qt.AlignTop)
44        self.setRenderHint(QPainter.Antialiasing)
45
46    def addSignalList(self, outWidget, inWidget):
47        self.scene().clear()
48        outputs, inputs = outWidget.widgetInfo.outputs, inWidget.widgetInfo.inputs
49        outIcon, inIcon = self.canvasDlg.getWidgetIcon(outWidget.widgetInfo), self.canvasDlg.getWidgetIcon(inWidget.widgetInfo)
50        self.lines = []
51        self.outBoxes = []
52        self.inBoxes = []
53        self.texts = []
54        xSpaceBetweenWidgets = 100  # space between widgets
55        xWidgetOff = 10             # offset for widget position
56        yWidgetOffTop = 10          # offset for widget position
57        yWidgetOffBottom = 30       # offset for widget position
58        ySignalOff = 10             # space between the top of the widget and first signal
59        ySignalSpace = 50           # space between two neighbouring signals
60        ySignalSize = 20            # height of the signal box
61        xSignalSize = 20            # width of the signal box
62        xIconOff = 10
63        iconSize = 48
64
65        count = max(len(inputs), len(outputs))
66        height = max ((count)*ySignalSpace, 70)
67
68        # calculate needed sizes of boxes to show text
69        maxLeft = 0
70        for i in range(len(inputs)):
71            maxLeft = max(maxLeft, self.getTextWidth("("+inputs[i].name+")", 1))
72            maxLeft = max(maxLeft, self.getTextWidth(inputs[i].type))
73
74        maxRight = 0
75        for i in range(len(outputs)):
76            maxRight = max(maxRight, self.getTextWidth("("+outputs[i].name+")", 1))
77            maxRight = max(maxRight, self.getTextWidth(outputs[i].type))
78
79        width = max(maxLeft, maxRight) + 70 # we add 70 to show icons beside signal names
80
81        # show boxes
82        pen = QPen(QBrush(QColor(125, 162, 206, 192)), 1, Qt.SolidLine, Qt.RoundCap)
83        brush = QBrush(QColor(217, 232, 252, 192))
84        self.outWidget = QGraphicsRectItem(xWidgetOff, yWidgetOffTop, width, height, None, self.dlg.canvas)
85        self.outWidget.setBrush(brush)
86        self.outWidget.setPen(pen)
87        self.outWidget.setZValue(-100)
88
89        self.inWidget = QGraphicsRectItem(xWidgetOff + width + xSpaceBetweenWidgets, yWidgetOffTop, width, height, None, self.dlg.canvas)
90        self.inWidget.setBrush(brush)
91        self.inWidget.setPen(pen)
92        self.inWidget.setZValue(-100)
93       
94        canvasPicsDir  = os.path.join(self.canvasDlg.canvasDir, "icons")
95        if os.path.exists(os.path.join(canvasPicsDir, "frame.png")):
96            widgetBack = QPixmap(os.path.join(canvasPicsDir, "frame.png"))
97        else:
98            widgetBack = outWidget.imageFrame
99
100        # if icons -> show them
101        if outIcon:
102#            frame = QGraphicsPixmapItem(widgetBack, None, self.dlg.canvas)
103#            frame.setPos(xWidgetOff + xIconOff, yWidgetOffTop + height/2.0 - frame.pixmap().width()/2.0)
104            self.outWidgetIcon = QGraphicsPixmapItem(outIcon.pixmap(iconSize, iconSize), None, self.dlg.canvas)
105#            self.outWidgetIcon.setPos(xWidgetOff + xIconOff, yWidgetOffTop + height/2.0 - self.outWidgetIcon.pixmap().width()/2.0)
106            self.outWidgetIcon.setPos(xWidgetOff + xIconOff, yWidgetOffTop + xIconOff)
107       
108        if inIcon:
109#            frame = QGraphicsPixmapItem(widgetBack, None, self.dlg.canvas)
110#            frame.setPos(xWidgetOff + xSpaceBetweenWidgets + 2*width - xIconOff - frame.pixmap().width(), yWidgetOffTop + height/2.0 - frame.pixmap().width()/2.0)
111            self.inWidgetIcon = QGraphicsPixmapItem(inIcon.pixmap(iconSize, iconSize), None, self.dlg.canvas)
112#            self.inWidgetIcon.setPos(xWidgetOff + xSpaceBetweenWidgets + 2*width - xIconOff - self.inWidgetIcon.pixmap().width(), yWidgetOffTop + height/2.0 - self.inWidgetIcon.pixmap().width()/2.0)
113            self.inWidgetIcon.setPos(xWidgetOff + xSpaceBetweenWidgets + 2*width - xIconOff - self.inWidgetIcon.pixmap().width(), yWidgetOffTop + xIconOff)
114
115        # show signal boxes and text labels
116        #signalSpace = (count)*ySignalSpace
117        signalSpace = height
118        for i in range(len(outputs)):
119            y = yWidgetOffTop + ((i+1)*signalSpace)/float(len(outputs)+1)
120            box = QGraphicsRectItem(xWidgetOff + width, y - ySignalSize/2.0, xSignalSize, ySignalSize, None, self.dlg.canvas)
121            box.setBrush(QBrush(QColor(0,0,255)))
122            box.setZValue(200)
123            self.outBoxes.append((outputs[i].name, box))
124
125            self.texts.append(MyCanvasText(self.dlg.canvas, outputs[i].name, xWidgetOff + width - 8, y - 7, Qt.AlignRight | Qt.AlignVCenter, bold =1, show=1))
126            self.texts.append(MyCanvasText(self.dlg.canvas, outputs[i].type, xWidgetOff + width - 8, y + 7, Qt.AlignRight | Qt.AlignVCenter, bold =0, show=1))
127
128        for i in range(len(inputs)):
129            y = yWidgetOffTop + ((i+1)*signalSpace)/float(len(inputs)+1)
130            box = QGraphicsRectItem(xWidgetOff + width + xSpaceBetweenWidgets - xSignalSize, y - ySignalSize/2.0, xSignalSize, ySignalSize, None, self.dlg.canvas)
131            box.setBrush(QBrush(QColor(0,0,255)))
132            box.setZValue(200)
133            self.inBoxes.append((inputs[i].name, box))
134
135            self.texts.append(MyCanvasText(self.dlg.canvas, inputs[i].name, xWidgetOff + width + xSpaceBetweenWidgets + 8, y - 7, Qt.AlignLeft | Qt.AlignVCenter, bold =1, show=1))
136            self.texts.append(MyCanvasText(self.dlg.canvas, inputs[i].type, xWidgetOff + width + xSpaceBetweenWidgets + 8, y + 7, Qt.AlignLeft | Qt.AlignVCenter, bold =0, show=1))
137
138        self.texts.append(MyCanvasText(self.dlg.canvas, outWidget.caption, xWidgetOff + width/2.0, yWidgetOffTop + height + 5, Qt.AlignHCenter | Qt.AlignTop, bold =1, show=1))
139        self.texts.append(MyCanvasText(self.dlg.canvas, inWidget.caption, xWidgetOff + width* 1.5 + xSpaceBetweenWidgets, yWidgetOffTop + height + 5, Qt.AlignHCenter | Qt.AlignTop, bold =1, show=1))
140
141        return (2*xWidgetOff + 2*width + xSpaceBetweenWidgets, yWidgetOffTop + height + yWidgetOffBottom)
142
143    def getTextWidth(self, text, bold = 0):
144        temp = QGraphicsSimpleTextItem(text, None, self.dlg.canvas)
145        if bold:
146            font = temp.font()
147            font.setBold(1)
148            temp.setFont(font)
149        temp.hide()
150        return temp.boundingRect().width()
151
152    # ###################################################################
153    # mouse button was pressed
154    def mousePressEvent(self, ev):
155        self.bMouseDown = 1
156        point = self.mapToScene(ev.pos())
157        activeItem = self.scene().itemAt(QPointF(ev.pos()))
158        if type(activeItem) == QGraphicsRectItem and activeItem not in [self.outWidget, self.inWidget]:
159            self.tempLine = QGraphicsLineItem(None, self.dlg.canvas)
160            self.tempLine.setLine(point.x(), point.y(), point.x(), point.y())
161            self.tempLine.setPen(QPen(QColor(200, 200, 200), 1))
162            self.tempLine.setZValue(-300)
163           
164        elif type(activeItem) == QGraphicsLineItem:
165            for (line, outName, inName, outBox, inBox) in self.lines:
166                if line == activeItem:
167                    self.dlg.removeLink(outName, inName)
168                    return
169
170    # ###################################################################
171    # mouse button was released #########################################
172    def mouseMoveEvent(self, ev):
173        if self.tempLine:
174            curr = self.mapToScene(ev.pos())
175            start = self.tempLine.line().p1()
176            self.tempLine.setLine(start.x(), start.y(), curr.x(), curr.y())
177            self.scene().update()
178
179    # ###################################################################
180    # mouse button was released #########################################
181    def mouseReleaseEvent(self, ev):
182        if self.tempLine:
183            activeItem = self.scene().itemAt(QPointF(ev.pos()))
184            if type(activeItem) == QGraphicsRectItem:
185                activeItem2 = self.scene().itemAt(self.tempLine.line().p1())
186                if activeItem.x() < activeItem2.x(): outBox = activeItem; inBox = activeItem2
187                else:                                outBox = activeItem2; inBox = activeItem
188                outName = None; inName = None
189                for (name, box) in self.outBoxes:
190                    if box == outBox: outName = name
191                for (name, box) in self.inBoxes:
192                    if box == inBox: inName = name
193                if outName != None and inName != None:
194                    self.dlg.addLink(outName, inName)
195
196            self.tempLine.hide()
197            self.tempLine = None
198            self.scene().update()
199
200
201    def addLink(self, outName, inName):
202        outBox = None; inBox = None
203        for (name, box) in self.outBoxes:
204            if name == outName: outBox = box
205        for (name, box) in self.inBoxes:
206            if name == inName : inBox  = box
207        if outBox == None or inBox == None:
208            print "error adding link. Data = ", outName, inName
209            return
210        line = QGraphicsLineItem(None, self.dlg.canvas)
211        outRect = outBox.rect()
212        inRect = inBox.rect()
213        line.setLine(outRect.x() + outRect.width()-2, outRect.y() + outRect.height()/2.0, inRect.x()+2, inRect.y() + inRect.height()/2.0)
214        line.setPen(QPen(QColor(160, 160, 160), 5))
215        line.setZValue(100)
216        self.scene().update()
217        self.lines.append((line, outName, inName, outBox, inBox))
218
219
220    def removeLink(self, outName, inName):
221        for (line, outN, inN, outBox, inBox) in self.lines:
222            if outN == outName and inN == inName:
223                line.hide()
224                self.lines.remove((line, outN, inN, outBox, inBox))
225                self.scene().update()
226                return
227
228
229# #######################################
230# # Signal dialog - let the user select active signals between two widgets
231# #######################################
232class SignalDialog(QDialog):
233    def __init__(self, canvasDlg, *args):
234        QDialog.__init__(self, *args)
235        self.setAttribute(Qt.WA_DeleteOnClose, True)
236        self.canvasDlg = canvasDlg
237
238        self.signals = []
239        self._links = []
240        self.allSignalsTaken = 0
241        self.signalManager = self.canvasDlg.schema.signalManager
242
243        # GUI
244        self.setWindowTitle('Connect Signals')
245        self.setLayout(QVBoxLayout())
246
247        self.canvasGroup = OWGUI.widgetBox(self, 1)
248        self.canvas = QGraphicsScene(0,0,1000,1000)
249        self.canvasView = SignalCanvasView(self, self.canvasDlg, self.canvas, self.canvasGroup)
250        self.canvasGroup.layout().addWidget(self.canvasView)
251
252        buttons = OWGUI.widgetBox(self, orientation = "horizontal", sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed))
253
254        self.buttonHelp = OWGUI.button(buttons, self, "&Help")
255        buttons.layout().addStretch(1)
256        self.buttonClearAll = OWGUI.button(buttons, self, "Clear &All", callback = self.clearAll)
257        self.buttonOk = OWGUI.button(buttons, self, "&OK", callback = self.accept)
258        self.buttonOk.setAutoDefault(1)
259        self.buttonOk.setDefault(1)
260        self.buttonCancel = OWGUI.button(buttons, self, "&Cancel", callback = self.reject)
261
262    def clearAll(self):
263        while self._links != []:
264            self.removeLink(self._links[0][0], self._links[0][1])
265
266    def setOutInWidgets(self, outWidget, inWidget):
267        self.outWidget = outWidget
268        self.inWidget = inWidget
269        (width, height) = self.canvasView.addSignalList(outWidget, inWidget)
270        self.canvas.setSceneRect(0, 0, width, height)
271        self.resize(width+50, height+80)
272
273    def countCompatibleConnections(self, outputs, inputs, outInstance, inInstance, outType, inType):
274        count = 0
275        for outS in outputs:
276            if outInstance.getOutputType(outS.name) == None: continue  # ignore if some signals don't exist any more, since we will report error somewhere else
277            if not issubclass(outInstance.getOutputType(outS.name), outType): continue
278            for inS in inputs:
279                if inInstance.getOutputType(inS.name) == None: continue  # ignore if some signals don't exist any more, since we will report error somewhere else
280                if not issubclass(inType, inInstance.getInputType(inS.name)): continue
281                if issubclass(outInstance.getOutputType(outS.name), inInstance.getInputType(inS.name)): count+= 1
282
283        return count
284
285    def existsABetterLink(self, outSignal, inSignal, outSignals, inSignals):
286        existsBetter = 0
287
288        betterOutSignal = None; betterInSignal = None
289        for outS in outSignals:
290            for inS in inSignals:
291                if (outS.name != outSignal.name and outS.name == inSignal.name and outS.type == inSignal.type) or (inS.name != inSignal.name and inS.name == outSignal.name and inS.type == outSignal.type):
292                    existsBetter = 1
293                    betterOutSignal = outS
294                    betterInSignal = inS
295        return existsBetter, betterOutSignal, betterInSignal
296
297
298    def getPossibleConnections(self, outputs, inputs):
299        possibleLinks = []
300        for outS in outputs:
301            outType = self.outWidget.instance.getOutputType(outS.name)
302            if outType == None:     #print "Unable to find signal type for signal %s. Check the definition of the widget." % (outS.name)
303                continue
304            for inS in inputs:
305                inType = self.inWidget.instance.getInputType(inS.name)
306                if inType == None:
307                    continue        #print "Unable to find signal type for signal %s. Check the definition of the widget." % (inS.name)
308                if issubclass(outType, inType):
309                    possibleLinks.append((outS.name, inS.name))
310                if issubclass(inType, outType):
311                    possibleLinks.append((outS.name, inS.name))
312        return possibleLinks
313
314    def addDefaultLinks(self):
315        canConnect = 0
316        addedInLinks = []
317        addedOutLinks = []
318        self.multiplePossibleConnections = 0    # can we connect some signal with more than one widget
319
320        minorInputs = [signal for signal in self.inWidget.widgetInfo.inputs if not signal.default]
321        majorInputs = [signal for signal in self.inWidget.widgetInfo.inputs if signal.default]
322        minorOutputs = [signal for signal in self.outWidget.widgetInfo.outputs if not signal.default]
323        majorOutputs = [signal for signal in self.outWidget.widgetInfo.outputs if signal.default]
324
325        inConnected = self.inWidget.getInConnectedSignalNames()
326        outConnected = self.outWidget.getOutConnectedSignalNames()
327
328        # input connections that can be simultaneously connected to multiple outputs are not to be considered as already connected
329        for i in inConnected[::-1]:
330            if not self.inWidget.instance.signalIsOnlySingleConnection(i):
331                inConnected.remove(i)
332
333        for s in majorInputs + minorInputs:
334            if not self.inWidget.instance.hasInputName(s.name):
335                return -1
336        for s in majorOutputs + minorOutputs:
337            if not self.outWidget.instance.hasOutputName(s.name):
338                return -1
339
340        pl1 = self.getPossibleConnections(majorOutputs, majorInputs)
341        pl2 = self.getPossibleConnections(majorOutputs, minorInputs)
342        pl3 = self.getPossibleConnections(minorOutputs, majorInputs)
343        pl4 = self.getPossibleConnections(minorOutputs, minorInputs)
344
345        all = pl1 + pl2 + pl3 + pl4
346
347        if not all: return 0
348
349        # try to find a link to any inputs that hasn't been previously connected
350        self.allSignalsTaken = 1
351        for (o,i) in all:
352            if i not in inConnected:
353                all.remove((o,i))
354                all.insert(0, (o,i))
355                self.allSignalsTaken = 0       # we found an unconnected link. no need to show the signal dialog
356                break
357        self.addLink(all[0][0], all[0][1])  # add only the best link
358
359        # there are multiple possible connections if we have in the same priority class more than one possible unconnected link
360        for pl in [pl1, pl2, pl3, pl4]:
361            #if len(pl) > 1 and sum([i not in inConnected for (o,i) in pl]) > 1: # if we have more than one valid
362            if len(pl) > 1:     # if we have more than one valid
363                self.multiplePossibleConnections = 1
364            if len(pl) > 0:     # when we find a first non-empty list we stop searching
365                break
366        return len(all) > 0
367   
368    def addDefaultLinks(self):
369        self.multiplePossibleConnections = 0
370        candidates = self.signalManager.proposePossibleLinks(self.outWidget.instance, self.inWidget.instance)
371        if candidates:
372            ## If there are more candidates with max weights
373            maxW = max([w for _,_,w in candidates])
374            maxCandidates = [c for c in candidates if c[-1] == maxW]
375            if len(maxCandidates) > 1:
376                self.multiplePossibleConnections = 1
377            best = maxCandidates[0]
378            self.addLink(best[0].name, best[1].name) # add the best to the view
379            return True
380        else:
381            return 0
382           
383
384    def addLink(self, outName, inName):
385        if (outName, inName) in self._links: return 1
386
387        # check if correct types
388        outType = self.outWidget.instance.getOutputType(outName)
389        inType = self.inWidget.instance.getInputType(inName)
390        if not issubclass(outType, inType) and not issubclass(inType, outType): return 0 #TODO check this with signalManager.canConnect
391
392        inSignal = None
393        inputs = self.inWidget.widgetInfo.inputs
394        for i in range(len(inputs)):
395            if inputs[i].name == inName: inSignal = inputs[i]
396
397        # if inName is a single signal and connection already exists -> delete it
398        for (outN, inN) in self._links:
399            if inN == inName and inSignal.single:
400                self.removeLink(outN, inN)
401
402        self._links.append((outName, inName))
403        self.canvasView.addLink(outName, inName)
404        return 1
405
406
407    def removeLink(self, outName, inName):
408        if (outName, inName) in self._links:
409            self._links.remove((outName, inName))
410            self.canvasView.removeLink(outName, inName)
411
412    def getLinks(self):
413        return self._links
414
415
416class ColorIcon(QToolButton):
417    def __init__(self, parent, color):
418        QToolButton.__init__(self, parent)
419        self.color = color
420        self.setMaximumSize(20,20)
421        self.connect(self, SIGNAL("clicked()"), self.showColorDialog)
422        self.updateColor()
423
424    def updateColor(self):
425        pixmap = QPixmap(16,16)
426        painter = QPainter()
427        painter.begin(pixmap)
428        painter.setPen(QPen(self.color))
429        painter.setBrush(QBrush(self.color))
430        painter.drawRect(0, 0, 16, 16);
431        painter.end()
432        self.setIcon(QIcon(pixmap))
433        self.setIconSize(QSize(16,16))
434
435
436    def drawButtonLabel(self, painter):
437        painter.setBrush(QBrush(self.color))
438        painter.setPen(QPen(self.color))
439        painter.drawRect(3, 3, self.width()-6, self.height()-6)
440
441    def showColorDialog(self):
442        color = QColorDialog.getColor(self.color, self)
443        if color.isValid():
444            self.color = color
445            self.updateColor()
446            self.repaint()
447
448# canvas dialog
449class CanvasOptionsDlg(QDialog):
450    def __init__(self, canvasDlg, *args):
451        apply(QDialog.__init__,(self,) + args)
452        self.canvasDlg = canvasDlg
453        self.settings = dict(canvasDlg.settings)        # create a copy of the settings dict. in case we accept the dialog, we update the canvasDlg.settings with this dict
454        if sys.platform == "darwin":
455            self.setWindowTitle("Preferences")
456        else:
457            self.setWindowTitle("Canvas Options")
458        self.topLayout = QVBoxLayout(self)
459        self.topLayout.setSpacing(0)
460        self.resize(300,300)
461#        self.toAdd = []
462#        self.toRemove = []
463
464        self.tabs = QTabWidget(self)
465        GeneralTab = OWGUI.widgetBox(self.tabs, margin = 4)
466        ExceptionsTab = OWGUI.widgetBox(self.tabs, margin = 4)
467        TabOrderTab = OWGUI.widgetBox(self.tabs, margin = 4)
468
469        self.tabs.addTab(GeneralTab, "General")
470        self.tabs.addTab(ExceptionsTab, "Exception handling")
471        self.tabs.addTab(TabOrderTab, "Widget tab order")
472
473        # #################################################################
474        # GENERAL TAB
475        generalBox = OWGUI.widgetBox(GeneralTab, "General Options")
476        self.snapToGridCB = OWGUI.checkBox(generalBox, self.settings, "snapToGrid", "Snap widgets to grid", debuggingEnabled = 0)
477        self.enableCanvasDropShadowsCB = OWGUI.checkBox(generalBox, self.settings, "enableCanvasDropShadows", "Enable drop shadows in canvas", debuggingEnabled = 0)
478        self.writeLogFileCB  = OWGUI.checkBox(generalBox, self.settings, "writeLogFile", "Save content of the Output window to a log file", debuggingEnabled = 0)
479        self.showSignalNamesCB = OWGUI.checkBox(generalBox, self.settings, "showSignalNames", "Show signal names between widgets", debuggingEnabled = 0)
480        self.dontAskBeforeCloseCB= OWGUI.checkBox(generalBox, self.settings, "dontAskBeforeClose", "Don't ask to save schema before closing", debuggingEnabled = 0)
481        self.saveWidgetsPositionCB = OWGUI.checkBox(generalBox, self.settings, "saveWidgetsPosition", "Save size and position of widgets", debuggingEnabled = 0)
482        self.useContextsCB = OWGUI.checkBox(generalBox, self.settings, "useContexts", "Use context settings")
483
484        validator = QIntValidator(self)
485        validator.setRange(0,10000)
486
487        hbox1 = OWGUI.widgetBox(GeneralTab, orientation = "horizontal")
488        hbox2 = OWGUI.widgetBox(GeneralTab, orientation = "horizontal")
489        canvasDlgSettings = OWGUI.widgetBox(hbox1, "Canvas Dialog Settings")
490#        schemeSettings = OWGUI.widgetBox(hbox1, "Scheme Settings")
491         
492#        self.widthSlider = OWGUI.qwtHSlider(canvasDlgSettings, self.settings, "canvasWidth", minValue = 300, maxValue = 1200, label = "Canvas width:  ", step = 50, precision = " %.0f px", debuggingEnabled = 0)
493#        self.heightSlider = OWGUI.qwtHSlider(canvasDlgSettings, self.settings, "canvasHeight", minValue = 300, maxValue = 1200, label = "Canvas height:  ", step = 50, precision = " %.0f px", debuggingEnabled = 0)
494#        OWGUI.separator(canvasDlgSettings)
495       
496        items = [str(n) for n in QStyleFactory.keys()]
497        itemsLower = [s.lower() for s in items]
498        ind = itemsLower.index(self.settings.get("style", "Windows").lower())
499        self.settings["style"] = items[ind]
500        OWGUI.comboBox(canvasDlgSettings, self.settings, "style", label = "Window style:", orientation = "horizontal", items = [str(n) for n in QStyleFactory.keys()], sendSelectedValue = 1, debuggingEnabled = 0)
501        OWGUI.checkBox(canvasDlgSettings, self.settings, "useDefaultPalette", "Use style's standard palette", debuggingEnabled = 0)
502       
503        OWGUI.separator(canvasDlgSettings)
504        OWGUI.comboBox(canvasDlgSettings, self.settings, "widgetListType", label="Toolbox style:", orientation="horizontal",
505                       items = ["Tool box", "Tree view", "Tree view (no icons)", "Tabs without labels", "Tabs with labels"], debuggingEnabled=0)
506       
507#        if canvasDlg:
508#            selectedWidgetBox = OWGUI.widgetBox(schemeSettings, orientation = "horizontal")
509#            self.selectedWidgetIcon = ColorIcon(selectedWidgetBox, canvasDlg.widgetSelectedColor)
510#            selectedWidgetBox.layout().addWidget(self.selectedWidgetIcon)
511#            selectedWidgetLabel = OWGUI.widgetLabel(selectedWidgetBox, " Selected widget")
512#
513#            activeWidgetBox = OWGUI.widgetBox(schemeSettings, orientation = "horizontal")
514#            self.activeWidgetIcon = ColorIcon(activeWidgetBox, canvasDlg.widgetActiveColor)
515#            activeWidgetBox.layout().addWidget(self.activeWidgetIcon)
516#            selectedWidgetLabel = OWGUI.widgetLabel(activeWidgetBox, " Active widget")
517#
518#            lineBox = OWGUI.widgetBox(schemeSettings, orientation = "horizontal")
519#            self.lineIcon = ColorIcon(lineBox, canvasDlg.lineColor)
520#            lineBox.layout().addWidget(self.lineIcon)
521#            selectedWidgetLabel = OWGUI.widgetLabel(lineBox, " Lines")
522#           
523#        OWGUI.separator(schemeSettings)
524#        items = ["%d x %d" % (v,v) for v in self.canvasDlg.schemeIconSizeList]
525#        val = min(len(items)-1, self.settings['schemeIconSize'])
526#        self.schemeIconSizeCombo = OWGUI.comboBoxWithCaption(schemeSettings, self.settings, 'schemeIconSize', "Scheme icon size:", items = items, tooltip = "Set the size of the widget icons on the scheme", debuggingEnabled = 0)
527       
528        GeneralTab.layout().addStretch(1)
529
530        # #################################################################
531        # EXCEPTION TAB
532        exceptions = OWGUI.widgetBox(ExceptionsTab, "Exceptions")
533        #self.catchExceptionCB = QCheckBox('Catch exceptions', exceptions)
534        self.focusOnCatchExceptionCB = OWGUI.checkBox(exceptions, self.settings, "focusOnCatchException", 'Show output window on exception')
535        self.printExceptionInStatusBarCB = OWGUI.checkBox(exceptions, self.settings, "printExceptionInStatusBar", 'Print last exception in status bar')
536
537        output = OWGUI.widgetBox(ExceptionsTab, "System output")
538        #self.catchOutputCB = QCheckBox('Catch system output', output)
539        self.focusOnCatchOutputCB = OWGUI.checkBox(output, self.settings, "focusOnCatchOutput", 'Focus output window on system output')
540        self.printOutputInStatusBarCB = OWGUI.checkBox(output, self.settings, "printOutputInStatusBar", 'Print last system output in status bar')
541
542        hboxExc = OWGUI.widgetBox(ExceptionsTab, orientation="horizontal")
543        outputCanvas = OWGUI.widgetBox(hboxExc, "Canvas Info Handling")
544        outputWidgets = OWGUI.widgetBox(hboxExc, "Widget Info Handling")
545        self.ocShow = OWGUI.checkBox(outputCanvas, self.settings, "ocShow", 'Show icon above widget for...')
546        indent = OWGUI.checkButtonOffsetHint(self.ocShow)
547        self.ocInfo = OWGUI.checkBox(OWGUI.indentedBox(outputCanvas, indent), self.settings, "ocInfo", 'Information')
548        self.ocWarning = OWGUI.checkBox(OWGUI.indentedBox(outputCanvas, indent), self.settings, "ocWarning", 'Warnings')
549        self.ocError = OWGUI.checkBox(OWGUI.indentedBox(outputCanvas, indent), self.settings, "ocError", 'Errors')
550
551        self.owShow = OWGUI.checkBox(outputWidgets, self.settings, "owShow", 'Show statusbar info for...')
552        self.owInfo = OWGUI.checkBox(OWGUI.indentedBox(outputWidgets, indent), self.settings, "owInfo", 'Information')
553        self.owWarning = OWGUI.checkBox(OWGUI.indentedBox(outputWidgets, indent), self.settings, "owWarning", 'Warnings')
554        self.owError = OWGUI.checkBox(OWGUI.indentedBox(outputWidgets, indent), self.settings, "owError", 'Errors')
555
556        verbosityBox = OWGUI.widgetBox(ExceptionsTab, "Verbosity", orientation = "horizontal")
557        verbosityBox.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum))
558        self.verbosityCombo = OWGUI.comboBox(verbosityBox, self.settings, "outputVerbosity", label = "Set level of widget output: ", orientation='horizontal', items=["Small", "Medium", "High"])
559        ExceptionsTab.layout().addStretch(1)
560
561        # #################################################################
562        # TAB ORDER TAB
563        tabOrderBox = OWGUI.widgetBox(TabOrderTab, "Set Order of Widget Categories", orientation = "horizontal", sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum))
564        self.tabOrderList = QListWidget(tabOrderBox)
565        self.tabOrderList.setAcceptDrops(True)
566
567        tabOrderBox.layout().addWidget(self.tabOrderList)
568        self.tabOrderList.setSelectionMode(QListWidget.SingleSelection)
569       
570        ind = 0
571        for (name, show) in self.settings["WidgetTabs"]:
572            if self.canvasDlg.widgetRegistry.has_key(name):
573                self.tabOrderList.addItem(name)
574                self.tabOrderList.item(ind).setCheckState(show and Qt.Checked or Qt.Unchecked)
575                ind+=1
576
577        w = OWGUI.widgetBox(tabOrderBox, sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding))
578        self.upButton = OWGUI.button(w, self, "Up", callback = self.moveUp)
579        self.downButton = OWGUI.button(w, self, "Down", callback = self.moveDown)
580#        w.layout().addSpacing(20)
581#        self.addButton = OWGUI.button(w, self, "Add", callback = self.addCategory)
582#        self.removeButton = OWGUI.button(w, self, "Remove", callback = self.removeCategory)
583#        self.removeButton.setEnabled(0)
584        w.layout().addStretch(1)
585
586        # OK, Cancel buttons
587        hbox = OWGUI.widgetBox(self, orientation = "horizontal", sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed))
588        hbox.layout().addStretch(1)
589        self.okButton = OWGUI.button(hbox, self, "OK", callback = self.accept)
590        self.cancelButton = OWGUI.button(hbox, self, "Cancel", callback = self.reject)
591        self.connect(self.tabOrderList, SIGNAL("currentRowChanged(int)"), self.enableDisableButtons)
592
593        self.topLayout.addWidget(self.tabs)
594        self.topLayout.addWidget(hbox)
595
596
597    def accept(self):
598#        self.settings["widgetSelectedColor"] = self.selectedWidgetIcon.color.getRgb()
599#        self.settings["widgetActiveColor"]   = self.activeWidgetIcon.color.getRgb()
600#        self.settings["lineColor"]           = self.lineIcon.color.getRgb()
601        QDialog.accept(self)
602       
603       
604
605    # move selected widget category up
606    def moveUp(self):
607        for i in range(1, self.tabOrderList.count()):
608            if self.tabOrderList.item(i).isSelected():
609                item = self.tabOrderList.takeItem(i)
610                for j in range(self.tabOrderList.count()): self.tabOrderList.item(j).setSelected(0)
611                self.tabOrderList.insertItem(i-1, item)
612                item.setSelected(1)
613
614    # move selected widget category down
615    def moveDown(self):
616        for i in range(self.tabOrderList.count()-2,-1,-1):
617            if self.tabOrderList.item(i).isSelected():
618                item = self.tabOrderList.takeItem(i)
619                for j in range(self.tabOrderList.count()): self.tabOrderList.item(j).setSelected(0)
620                self.tabOrderList.insertItem(i+1, item)
621                item.setSelected(1)
622
623    def enableDisableButtons(self, itemIndex):
624        self.upButton.setEnabled(itemIndex > 0)
625        self.downButton.setEnabled(itemIndex < self.tabOrderList.count()-1)
626        catName = str(self.tabOrderList.currentItem().text())
627        if not self.canvasDlg.widgetRegistry.has_key(catName): return
628#        self.removeButton.setEnabled( all([os.path.normpath(self.canvasDlg.widgetDir) not in os.path.normpath(x.directory) and
629#                                           os.path.normpath(self.canvasDlg.addOnsDir) not in os.path.normpath(x.directory)
630#                                             for x in self.canvasDlg.widgetRegistry[catName].values()]))
631        #self.removeButton.setEnabled(1)
632
633#    def addCategory(self):
634#        dir = str(QFileDialog.getExistingDirectory(self, "Select the folder that contains the add-on:"))
635#        if dir != "":
636#            if os.path.split(dir)[1] == "widgets":     # register a dir above the dir that contains the widget folder
637#                dir = os.path.split(dir)[0]
638#            if os.path.exists(os.path.join(dir, "widgets")):
639#                name = os.path.split(dir)[1]
640#                self.toAdd.append((name, dir))
641#                self.tabOrderList.addItem(name)
642#                self.tabOrderList.item(self.tabOrderList.count()-1).setCheckState(Qt.Checked)
643#            else:
644#                QMessageBox.information( None, "Information", 'The specified folder does not seem to contain an Orange add-on.', QMessageBox.Ok + QMessageBox.Default)
645#           
646#       
647#    def removeCategory(self):
648#        curCat = str(self.tabOrderList.item(self.tabOrderList.currentRow()).text())
649#        if QMessageBox.warning(self,'Orange Canvas', "Unregister widget category '%s' from Orange canvas?\nThis will not remove any files." % curCat, QMessageBox.Ok , QMessageBox.Cancel | QMessageBox.Default | QMessageBox.Escape) == QMessageBox.Ok:
650#            self.toRemove.append((curCat, self.canvasDlg.widgetRegistry[curCat]))
651#            item = self.tabOrderList.takeItem(self.tabOrderList.row(self.tabOrderList.currentItem()))
652#            #if item: item.setHidden(1)
653
654class KeyEdit(QLineEdit):
655    def __init__(self, parent, key, invdict, widget, invInvDict):
656        QLineEdit.__init__(self, parent)
657        self.setText(key)
658        #self.setReadOnly(True)
659        self.invdict = invdict
660        self.widget = widget
661        self.invInvDict = invInvDict
662
663    def keyPressEvent(self, e):
664        if e.key() == Qt.Key_Delete or e.key() == Qt.Key_Backspace:
665            pressed = "<none>"
666            self.setText(pressed)
667            prevkey = self.invdict.get(self.widget)
668            if prevkey:
669                del self.invdict[self.widget]
670                del self.invInvDict[prevkey]
671            return
672
673        if e.key() not in range(32, 128): # + range(Qt.Key_F1, Qt.Key_F35+1): -- this wouldn't work, see the line below, and also writing to file etc.
674            e.ignore()
675            return
676
677        pressed = "-".join(filter(None, [e.modifiers() & x and y for x, y in [(Qt.ControlModifier, "Ctrl"), (Qt.AltModifier, "Alt")]]) + [chr(e.key())])
678
679        assigned = self.invInvDict.get(pressed, None)
680        if assigned and assigned != self and QMessageBox.question(self, "Confirmation", "'%(pressed)s' is already assigned to '%(assigned)s'. Override?" % {"pressed": pressed, "assigned": assigned.widget.name}, QMessageBox.Yes | QMessageBox.Default, QMessageBox.No | QMessageBox.Escape) == QMessageBox.No:
681            return
682       
683        if assigned:
684            assigned.setText("<none>")
685            del self.invdict[assigned.widget]
686        self.setText(pressed)
687        self.invdict[self.widget] = pressed
688        self.invInvDict[pressed] = self
689
690class AddOnManagerSummary(QDialog):
691    def __init__(self, add, remove, upgrade, *args):
692        apply(QDialog.__init__,(self,) + args)
693        self.setWindowTitle("Pending Actions")
694        self.topLayout = QVBoxLayout(self)
695        self.topLayout.setSpacing(10)
696        self.resize(200, 0)
697       
698        OWGUI.label(self, self, "If you confirm, the following actions will take place:")
699
700        self.memo = memo = QTextEdit(self)
701        self.layout().addWidget(memo)
702        memo.setReadOnly(True)
703        memo.setFrameStyle(QFrame.NoFrame)
704        pal = QPalette()
705        pal.setColor(QPalette.Base, Qt.transparent)
706        memo.setPalette(pal)
707        memo.setLineWrapMode(QTextEdit.WidgetWidth)
708        memo.setWordWrapMode(QTextOption.WordWrap)
709        QObject.connect(memo.document().documentLayout(),
710                       SIGNAL("documentSizeChanged(const QSizeF &)"),
711                        lambda docSize: self.updateMinSize(docSize))
712        actions = []
713        for ao in add:
714            actions.append("Install %s." % ao)
715        for ao in remove:
716            actions.append("Remove %s." % ao)
717        for ao in upgrade:
718            actions.append("Upgrade %s." % ao)
719        memo.setText("\n".join(actions))
720       
721        self.layout().addStretch(1)
722        self.setSizePolicy( QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Minimum) )
723
724        hbox = OWGUI.widgetBox(self, orientation = "horizontal", sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed))
725        hbox.layout().addStretch(1)
726        self.okButton = OWGUI.button(hbox, self, "OK", callback = self.accept)
727        self.cancelButton = OWGUI.button(hbox, self, "Cancel", callback = self.reject)
728        self.okButton.setDefault(True)
729
730    def updateMinSize(self, documentSize):
731        self.memo.update()
732        self.memo.setMinimumHeight(min(300, documentSize.height() + 2 * self.memo.frameWidth()))
733
734
735class AddOnManagerDialog(QDialog):
736    def __init__(self, canvasDlg, *args):
737        QDialog.__init__(self, *args)
738        self.setModal(True)
739
740        self.canvasDlg = canvasDlg
741        self.setWindowTitle("Add-on Management")
742        self.topLayout = QVBoxLayout(self)
743        self.topLayout.setSpacing(0)
744        self.resize(600,500)
745        self.layout().setSizeConstraint(QLayout.SetMinimumSize)
746        self.savetimefn = None
747        self.loadtimefn = None
748       
749        mainBox = OWGUI.widgetBox(self, orientation="vertical", sizePolicy=QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding))
750       
751        # View settings & Search
752       
753        self.groupByRepo = True
754        self.searchStr = ""
755        self.to_upgrade = set()
756
757        searchBox = OWGUI.widgetBox(mainBox, orientation = "horizontal", sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum))
758
759        self.eSearch = self.lineEditSearch(searchBox, self, "searchStr", None, 0, tooltip = "Type in to filter (search) add-ons.", callbackOnType=True, callback=self.searchCallback)
760       
761        # Repository & Add-on tree
762       
763        repos = OWGUI.widgetBox(mainBox, "Add-ons", orientation = "horizontal", sizePolicy=QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored))
764        repos.layout().setSizeConstraint(QLayout.SetMinimumSize)
765        self.lst = lst = QListWidget(repos)
766        lst.setMinimumWidth(200)
767        lst.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred))
768        repos.layout().addWidget(lst)
769        QObject.connect(lst, SIGNAL("itemChanged(QListWidgetItem *)"), self.cbToggled)
770        QObject.connect(lst, SIGNAL("currentItemChanged(QListWidgetItem *, QListWidgetItem *)"), self.currentItemChanged)
771
772        # Bottom info pane
773       
774        self.infoPane = infoPane = OWGUI.widgetBox(mainBox, orientation="vertical", sizePolicy=QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored))
775        infoPane.layout().setSizeConstraint(QLayout.SetMinimumSize)
776
777        pVerInstalled = OWGUI.widgetBox(infoPane, orientation="horizontal")
778        lblVerInstalled = OWGUI.label(pVerInstalled, self, "Installed version:", 150)
779        boldFont = lblVerInstalled.font()
780        boldFont.setWeight(QFont.Bold)
781        lblVerInstalled.setFont(boldFont)
782        self.lblVerInstalledValue = OWGUI.label(pVerInstalled, self, "-")
783        self.lblVerInstalledValue.setSizePolicy(QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum))
784        pVerInstalled.layout().addSpacing(10)
785        self.lblStatus = OWGUI.label(pVerInstalled, self, "")
786        self.lblStatus.setFont(boldFont)
787        pVerInstalled.layout().addStretch(1)
788
789        pVerAvail = OWGUI.widgetBox(infoPane, orientation="horizontal")
790        lblVerAvail = OWGUI.label(pVerAvail, self, "Available version:", 150)
791        lblVerAvail.setFont(boldFont)
792        self.lblVerAvailValue = OWGUI.label(pVerAvail, self, "")
793        pVerAvail.layout().addSpacing(10)
794        self.upgradeButton = OWGUI.button(pVerAvail, self, "Upgrade", callback = self.upgrade)
795        self.upgradeButton.setFixedHeight(lblVerAvail.height())
796        self.donotUpgradeButton = OWGUI.button(pVerAvail, self, "Do not upgrade", callback = self.donotUpgrade)
797        self.donotUpgradeButton.setFixedHeight(lblVerAvail.height())
798        pVerAvail.layout().addStretch(1)
799       
800        pInfoBtns = OWGUI.widgetBox(infoPane, orientation="horizontal", sizePolicy=QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed))
801        self.webButton = OWGUI.button(pInfoBtns, self, "Open webpage", callback = self.openWebPage)
802        self.docButton = OWGUI.button(pInfoBtns, self, "Open documentation", callback = self.openDocsPage)
803        self.listWidgetsButton = OWGUI.button(pInfoBtns, self, "List widgets", callback = self.listWidgets)
804        pInfoBtns.layout().addStretch(1)
805
806        self.lblDescription = lblDescription = QTextEdit(infoPane)
807        infoPane.layout().addWidget(lblDescription)
808        lblDescription.setReadOnly(True)
809        lblDescription.setFrameStyle(QFrame.NoFrame)
810        pal = QPalette()
811        pal.setColor(QPalette.Base, Qt.transparent)
812        lblDescription.setPalette(pal)
813        lblDescription.setLineWrapMode(QTextEdit.WidgetWidth)
814        lblDescription.setWordWrapMode(QTextOption.WordWrap)
815
816        # Right panel
817       
818        self.rightPanel = rightPanel = OWGUI.widgetBox(repos, orientation = "vertical", sizePolicy=QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding))
819        rightPanel.layout().setSizeConstraint(QLayout.SetMinimumSize)
820        self.reloadRepoButton = OWGUI.button(rightPanel, self, "Refresh list", callback = self.reloadRepo)
821        rightPanel.layout().addSpacing(15)
822        self.upgradeAllButton = OWGUI.button(rightPanel, self, "Upgrade All", callback = self.upgradeAll)
823        rightPanel.layout().addStretch(1)
824        for btn in rightPanel.children():
825            if btn.__class__ is QPushButton:
826                btn.setMinimumHeight(btn.height())
827       
828        # Buttons
829        self.hbox = hbox = OWGUI.widgetBox(mainBox, orientation = "horizontal", sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed))
830        busyBox = OWGUI.widgetBox(hbox, sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum))  # A humble stretch.
831        self.busyLbl = OWGUI.label(busyBox, self, "")
832        self.progress = QProgressBar(hbox, sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed))
833        hbox.layout().addWidget(self.progress)
834        self.progress.setVisible(False)
835        self.okButton = OWGUI.button(hbox, self, "OK", callback = self.accept)
836        self.cancelButton = OWGUI.button(hbox, self, "Cancel", callback = self.reject)
837        self.okButton.setDefault(True)
838
839        self.refreshView()
840       
841    def updateDescMinSize(self, documentSize):
842        self.lblDescription.update()
843        self.lblDescription.setMinimumHeight(min(300, documentSize.height() + 2 * self.lblDescription.frameWidth()))
844        pass
845   
846    def accept(self):
847        self.to_upgrade.difference_update(self.to_remove())
848        add, remove, upgrade = self.to_install(), self.to_remove(), self.to_upgrade
849        if len(add) + len(remove) + len(upgrade) > 0:
850            summary = AddOnManagerSummary(add, remove, upgrade, self)
851            if summary.exec_() == QDialog.Rejected:
852                return
853
854        self.busy(True)
855        self.repaint()
856
857        addons = Orange.utils.addons.open_addons(flag="r")
858
859        add, remove, upgrade = self.to_install(), self.to_remove(), self.to_upgrade
860
861        def errormessage(title, message, details=None, exc_info=None):
862            box = QMessageBox(QMessageBox.Critical, title, message,
863                              parent=self)
864
865            if details is not None:
866                box.setDetailedText(details)
867            elif exc_info:
868                import traceback
869                if isinstance(exc_info, tuple):
870                    details = traceback.format_exception(*(exc_info + (10,)))
871                else:
872                    details = traceback.format_exc(10)
873                box.setDetailedText(details)
874
875            return box.exec_()
876
877        def subprocesswait(process):
878            output = []
879            while process.poll() is None:
880                try:
881                    line = process.stdout.readline()
882                except IOError as ex:
883                    if ex.errno != 4:
884                        raise
885                else:
886                    output.append(line)
887                    qApp.processEvents(QEventLoop.ExcludeUserInputEvents)
888                    print line,
889
890            if process.returncode:
891                output = "".join(output)
892                output += process.stdout.read()
893
894                errormessage("Error",
895                             "'easy_install' exited with error code %i" %
896                             process.returncode,
897                             details=output)
898            return process.returncode
899
900        def easy_install(req):
901            try:
902                process = Orange.utils.addons.easy_install_process([req])
903            except (OSError, IOError):
904                # TODO: Should show some usefull message (executable not
905                # found, permission error, ...
906                raise
907            else:
908                subprocesswait(process)
909
910        for name in upgrade:
911            req = "{0}=={1}".format(
912                name, addons[name.lower()].available_version)
913
914            self.busy("Upgrading %s ..." % name)
915            self.progress.setRange(0, 0)
916            self.repaint()
917
918            easy_install(req)
919
920        for name in remove:
921            self.busy("Uninstalling %s ..." % name)
922            self.repaint()
923            try:
924                Orange.utils.addons.uninstall(name, self.pcb)
925            except Exception, e:
926                errormessage("Error",
927                             "Problem uninstalling add-on %s: %s" % (name, e),
928                             exc_info=True)
929
930        for name in add:
931            req = "{0}=={1}".format(
932                name, addons[name.lower()].available_version)
933
934            self.busy("Installing %s ..." % name)
935            self.progress.setRange(0, 0)
936            self.repaint()
937
938            easy_install(req)
939
940        if len(add) + len(upgrade) + len(remove) > 0:
941            QMessageBox.information(
942                self, "Restart Orange",
943                "Please restart Orange for changes to take effect.")
944
945        QDialog.accept(self)
946
947    def busy(self, b=True):
948        self.progress.setMaximum(1)
949        self.progress.setValue(0)
950        self.progress.setVisible(bool(b))
951        self.busyLbl.setText(b if isinstance(b, str) else "")
952        self.eSearch.setEnabled(not b)
953        self.lst.setEnabled(not b)
954        self.okButton.setEnabled(not b)
955        self.cancelButton.setEnabled(not b)
956        self.rightPanel.setEnabled(not b)
957        self.infoPane.setEnabled(not b)
958
959    def pcb(self, max, val):
960        self.progress.setMaximum(max)
961        self.progress.setValue(val)
962        qApp.processEvents(QEventLoop.ExcludeUserInputEvents)
963
964    def reloadRepo(self):
965        # Reload add-on list.
966        try:
967            self.busy("Reloading add-on repository ...")
968            self.repaint()
969            Orange.utils.addons.refresh_available_addons(progress_callback = self.pcb)
970            if self.savetimefn:
971                self.savetimefn(int(time.time()))
972        except Exception, e:
973            QMessageBox.critical(self, "Error", "Could not reload repository: %s." % e)
974        finally:
975            self.busy(False)
976        # Finally, refresh the tree on GUI.
977        self.refreshView()
978
979    def reloadQ(self):
980        #ask the user if he would like to reload the repository
981        lastRefresh = 0
982        if self.loadtimefn:
983            lastRefresh = self.loadtimefn()
984        t = time.time()
985        if t - lastRefresh > 7*24*3600 or Orange.utils.addons.addons_corrupted():
986            if Orange.utils.addons.addons_corrupted() or \
987               QMessageBox.question(self, "Refresh",
988                                    "List of available add-ons has not been refreshed for more than a week. Do you want to download the list now?",
989                                     QMessageBox.Yes | QMessageBox.Default,
990                                     QMessageBox.No | QMessageBox.Escape) == QMessageBox.Yes:
991                self.reloadRepo()
992           
993    def upgradeCandidates(self):
994        result = []
995        import Orange.utils.addons
996        with closing(Orange.utils.addons.open_addons()) as addons:
997            for ao in addons.values():
998                if ao.installed_version and ao.available_version and ao.installed_version != ao.available_version:
999                    result.append(ao.name)
1000        return result
1001   
1002    def upgradeAll(self):
1003        for candidate in self.upgradeCandidates():
1004            self.upgrade(candidate, refresh=False)
1005        self.refreshInfoPane()
1006
1007    def upgrade(self, name=None, refresh=True):
1008        if not name:
1009            name = self.getAddOnIdFromItem(self.lst.currentItem())
1010        self.to_upgrade.add(name)
1011        if refresh:
1012            self.refreshInfoPane()
1013
1014    def openWebPage(self):
1015        addon = self.getAddOnFromItem(self.lst.currentItem())
1016        if addon and addon.homepage:
1017            import webbrowser
1018            webbrowser.open(addon.homepage)
1019
1020    def openDocsPage(self):
1021        addon = self.getAddOnFromItem(self.lst.currentItem())
1022        if addon and addon.docs_url:
1023            import webbrowser
1024            webbrowser.open(addon.docs_url)
1025
1026    def listWidgets(self):
1027        addOn = self.getAddOnFromItem(self.lst.currentItem())
1028        if not addOn: return
1029        import Orange.utils.addons
1030        if addOn.__class__ is not Orange.utils.addons.OrangeAddOnInRepo: return
1031        if not addOn.repository.has_web_script: return
1032        self.canvasDlg.helpWindow.open("%s/addOnServer.py/%s/doc/widgets/" % (addOn.repository.url, addOn.filename), modal=True)
1033       
1034       
1035    def donotUpgrade(self):
1036        id = self.getAddOnIdFromItem(self.lst.currentItem())
1037        self.to_upgrade.remove(id)
1038        self.refreshInfoPane()
1039
1040    def cbToggled(self, item):
1041        ao = self.getAddOnFromItem(item)
1042        if ao and not has_pip and ao.installed_version and item.checkState()==Qt.Unchecked:
1043            QMessageBox.warning(self, "Unable to uninstall", "Pip is not installed on your system. Without it, automated removal of add-ons is not possible.\n\nInstall pip (try 'easy_install --user pip') and restart Orange to make this action possible.")
1044            item.setCheckState(Qt.Checked)
1045        self.refreshInfoPane(item)
1046
1047    def lineEditSearch(self, *args, **props):
1048        return OWGUI.lineEdit(*args, **props)
1049
1050    def getAddOnFromItem(self, item):
1051        return getattr(item, "addon", None)
1052
1053    def getAddOnIdFromItem(self, item):
1054        addon = self.getAddOnFromItem(item)
1055        return addon.name if addon else None
1056       
1057    def refreshInfoPane(self, item=None):
1058        if not item:
1059            item = self.lst.currentItem()
1060        addon = None
1061        if item:
1062            import Orange.utils.addons
1063            import orngEnviron
1064            addon = self.getAddOnFromItem(item)
1065        if addon:
1066            self.lblDescription.setText((addon.summary.strip() or "") +"\n"+ (addon.description.strip() or ""))
1067            self.lblVerAvailValue.setText(addon.available_version or "")
1068
1069            self.lblVerInstalledValue.setText(addon.installed_version or "-") #TODO Tell whether it's a system-wide installation
1070            self.upgradeButton.setVisible(bool(addon.installed_version and addon.installed_version!=addon.available_version) and addon.name not in self.to_upgrade) #TODO Disable if it's a system-wide installation
1071            self.donotUpgradeButton.setVisible(addon.name in self.to_upgrade)
1072            self.webButton.setVisible(bool(addon.homepage))
1073            self.docButton.setVisible(bool(addon.docs_url))
1074            self.listWidgetsButton.setVisible(False) #TODO A list of widgets is not available
1075
1076            if not addon.installed_version and item.checkState()==Qt.Checked:
1077                self.lblStatus.setText("marked for installation")
1078            elif addon.installed_version and item.checkState()!=Qt.Checked:
1079                self.lblStatus.setText("marked for removal")
1080            elif addon.name in self.to_upgrade:
1081                self.lblStatus.setText("marked for upgrade")
1082            else:
1083                self.lblStatus.setText("")
1084
1085            self.infoPane.setVisible(True)
1086        else:
1087            self.infoPane.setVisible(False)
1088        self.enableDisableButtons()
1089
1090    def enableDisableButtons(self):
1091        import Orange.utils.addons
1092        with closing(Orange.utils.addons.open_addons()) as addons:
1093            aos = addons.values()
1094            self.upgradeAllButton.setEnabled(any(ao.installed_version and ao.available_version and
1095                                                 ao.installed_version != ao.available_version and
1096                                                 ao.name not in self.to_upgrade for ao in aos))
1097       
1098    def currentItemChanged(self, new, previous):
1099        # Refresh info pane & button states
1100        self.refreshInfoPane(new)
1101
1102    def addAddOnsToTree(self, addon_dict, selected=None, to_install=[], to_remove=[]):
1103        # Sort alphabetically
1104        addons = sorted(list(addon_dict.items()),
1105                        key = lambda (name, ao): name)
1106
1107        for (i, (name, ao)) in enumerate(addons):
1108            item = QListWidgetItem()
1109            self.lst.addItem(item)
1110            item.setText(ao.name)
1111            item.setCheckState(Qt.Checked if ao.installed_version and not name in to_remove or name in to_install else Qt.Unchecked)
1112            item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsSelectable | Qt.ItemIsEnabled)
1113            item.addon = ao
1114            if name == selected:
1115                self.lst.setCurrentItem(item)
1116
1117            item.disableToggleSignal = False
1118
1119    def lst_items(self):
1120        for i in xrange(self.lst.count()):
1121            yield self.lst.item(i)
1122
1123    def to_install(self):
1124        return set([item.addon.name for item in self.lst_items()
1125                    if item.checkState()==Qt.Checked and not item.addon.installed_version])
1126
1127    def to_remove(self):
1128        return set([item.addon.name for item in self.lst_items()
1129                    if item.checkState()!=Qt.Checked and item.addon.installed_version])
1130
1131    def refreshView(self, selectedRegisteredAddOnId=None):
1132        import Orange
1133        # Save current item selection
1134        selected_addon = self.getAddOnIdFromItem(self.lst.currentItem())
1135        to_install = self.to_install()
1136        to_remove = self.to_remove()
1137        #TODO: Save the next repository selection too, in case the current one was deleted
1138
1139        # Clear the tree
1140        self.lst.clear()
1141       
1142        # Add repositories and add-ons
1143        with closing(Orange.utils.addons.open_addons()) as global_addons:
1144            addons = {}
1145            for name in Orange.utils.addons.search_index(self.searchStr):
1146                addons[name.lower()] = global_addons[name.lower()]
1147            self.addAddOnsToTree(addons, selected = selected_addon, to_install=to_install, to_remove=to_remove)
1148            self.refreshInfoPane()
1149
1150        #TODO Should we somehow show the legacy registered addons?
1151
1152    def searchCallback(self):
1153        self.refreshView()
1154   
1155# widget shortcuts dialog
1156class WidgetShortcutDlg(QDialog):
1157    def __init__(self, canvasDlg, *args):
1158        import orngTabs
1159
1160        apply(QDialog.__init__,(self,) + args)
1161        self.canvasDlg = canvasDlg
1162        self.setWindowTitle("Widget Shortcuts")
1163        self.setLayout(QVBoxLayout())
1164        self.layout().setSpacing(10)
1165        self.resize(700,500)
1166
1167        self.invDict = dict([(y, x) for x, y in canvasDlg.widgetShortcuts.items()])
1168        invInvDict = {}
1169
1170        self.tabs = QTabWidget(self)
1171       
1172        extraTabs = [(name, 1) for name in canvasDlg.widgetRegistry.keys() if name not in [tab for (tab, s) in canvasDlg.settings["WidgetTabs"]]]
1173        for tabName, show in canvasDlg.settings["WidgetTabs"] + extraTabs:
1174            if not canvasDlg.widgetRegistry.has_key(tabName):
1175                continue
1176            scrollArea = QScrollArea()
1177            self.tabs.addTab(scrollArea, tabName)
1178            #scrollArea.setWidgetResizable(1)       # you have to use this or set size to wtab manually - otherwise nothing gets shown
1179
1180            wtab = QWidget(self.tabs)
1181            scrollArea.setWidget(wtab)
1182
1183            widgets = [(int(widgetInfo.priority), name, widgetInfo) for (name, widgetInfo) in canvasDlg.widgetRegistry[tabName].items()]
1184            widgets.sort()
1185            rows = (len(widgets)+2) / 3
1186            layout = QGridLayout(wtab)
1187
1188            for i, (priority, name, widgetInfo) in enumerate(widgets):
1189                x = i / rows
1190                y = i % rows
1191
1192                hlayout = QHBoxLayout()
1193                mainBox = QWidget(wtab)
1194                mainBox.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed))
1195                mainBox.setLayout(hlayout)
1196                layout.addWidget(mainBox, y, x, Qt.AlignTop | Qt.AlignLeft)
1197                label = QLabel(wtab)
1198                label.setPixmap(canvasDlg.getWidgetIcon(widgetInfo).pixmap(40))
1199                hlayout.addWidget(label)
1200
1201                optionsw = QWidget(self)
1202                optionsw.setLayout(QVBoxLayout())
1203                hlayout.addWidget(optionsw)
1204                optionsw.layout().addStretch(1)
1205
1206                OWGUI.widgetLabel(optionsw, name)
1207                key = self.invDict.get(widgetInfo, "<none>")
1208                le = KeyEdit(optionsw, key, self.invDict, widgetInfo, invInvDict)
1209                optionsw.layout().addWidget(le)
1210                invInvDict[key] = le
1211                le.setFixedWidth(60)
1212
1213            wtab.resize(wtab.sizeHint())
1214
1215        # OK, Cancel buttons
1216        hbox = OWGUI.widgetBox(self, orientation = "horizontal", sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed))
1217        hbox.layout().addStretch(1)
1218        self.okButton = OWGUI.button(hbox, self, "OK", callback = self.accept)
1219        self.cancelButton = OWGUI.button(hbox, self, "Cancel", callback = self.reject)
1220        self.okButton.setDefault(True)
1221
1222        self.layout().addWidget(self.tabs)
1223        self.layout().addWidget(hbox)
1224
1225
1226class AboutDlg(QDialog):
1227    def __init__(self, *args):
1228        QDialog.__init__(self, *args)
1229        self.topLayout = QVBoxLayout(self)
1230#        self.setWindowFlags(Qt.Popup)       # Commented out, because it makes the window appear in the top-left corner on Linux
1231        self.setWindowTitle("About Orange")
1232       
1233        import orngEnviron
1234        import Orange
1235        import re
1236        logoImage = QPixmap(os.path.join(orngEnviron.directoryNames["canvasDir"], "icons", "splash.png"))
1237        logo = OWGUI.widgetLabel(self, "")
1238        logo.setPixmap(logoImage)
1239       
1240        OWGUI.widgetLabel(self, '<p align="center"><h2>Orange</h2></p>') 
1241       
1242        default_version_str = Orange.__version__
1243        built_on = re.findall("\((.*?)\)", Orange.orange.version)
1244        if built_on:
1245            built_on_str = " (built on " + built_on[0].split(",")[-1] + ")"
1246        else:
1247            built_on_str = ""
1248        try:
1249            import Orange.version as version
1250            short_version = version.short_version
1251            hg_revision = version.hg_revision
1252            OWGUI.widgetLabel(self, '<p align="center">version %s</p>' % (short_version + built_on_str))
1253            if not version.release:
1254                OWGUI.widgetLabel(self, '<p align="center">(hg revision %s)</p>' % (hg_revision))
1255        except ImportError:
1256            OWGUI.widgetLabel(self, '<p align="center">version %s</p>' % (default_version_str + built_on_str))
1257        OWGUI.widgetLabel(self, "" )
1258        #OWGUI.button(self, self, "Close", callback = self.accept)
1259        b = QDialogButtonBox(self)
1260        b.setCenterButtons(1)
1261        self.layout().addWidget(b)
1262        butt = b.addButton(QDialogButtonBox.Close)
1263        self.connect(butt, SIGNAL("clicked()"), self.accept)
1264       
1265
1266class saveApplicationDlg(QDialog):
1267    def __init__(self, *args):
1268        import Orange.utils.addons
1269       
1270        apply(QDialog.__init__,(self,) + args)
1271        self.setWindowTitle("Set Widget Order")
1272        self.setLayout(QVBoxLayout())
1273
1274        listbox = OWGUI.widgetBox(self, 1, orientation = "horizontal", sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum))
1275        self.tabOrderList = QListWidget(listbox)
1276        self.tabOrderList.setSelectionMode(QListWidget.SingleSelection)
1277        listbox.layout().addWidget(self.tabOrderList)
1278
1279        w = OWGUI.widgetBox(listbox, sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding))
1280        self.upButton = OWGUI.button(w, self, "Up", callback = self.moveUp)
1281        self.downButton = OWGUI.button(w, self, "Down", callback = self.moveDown)
1282        w.layout().addStretch(1)
1283
1284        hbox = OWGUI.widgetBox(self, orientation = "horizontal", sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed))
1285        OWGUI.button(hbox, self, "Add Separator", callback = self.insertSeparator)
1286        hbox.layout().addStretch(1)
1287        OWGUI.button(hbox, self, "&OK", callback = self.accept)
1288        OWGUI.button(hbox, self, "&Cancel", callback = self.reject)
1289
1290        self.resize(200,250)
1291
1292    def accept(self):
1293        self.shownWidgetList = [str(self.tabOrderList.item(i).text()) for i in range(self.tabOrderList.count())]
1294        QDialog.accept(self)
1295
1296    def insertSeparator(self):
1297        curr = self.tabOrderList.indexFromItem(self.tabOrderList.currentItem()).row()
1298        self.insertWidgetName("[Separator]", curr)
1299
1300    def insertWidgetName(self, name, index = -1):
1301        if index == -1:
1302            self.tabOrderList.addItem(name)
1303        else:
1304            self.tabOrderList.insertItem(index, name)
1305
1306    # move selected widget category up
1307    def moveUp(self):
1308        for i in range(1, self.tabOrderList.count()):
1309            if self.tabOrderList.item(i).isSelected():
1310                item = self.tabOrderList.takeItem(i)
1311                for j in range(self.tabOrderList.count()): self.tabOrderList.item(j).setSelected(0)
1312                self.tabOrderList.insertItem(i-1, item)
1313                item.setSelected(1)
1314
1315
1316    # move selected widget category down
1317    def moveDown(self):
1318        for i in range(self.tabOrderList.count()-2,-1,-1):
1319            if self.tabOrderList.item(i).isSelected():
1320                item = self.tabOrderList.takeItem(i)
1321                for j in range(self.tabOrderList.count()): self.tabOrderList.item(j).setSelected(0)
1322                self.tabOrderList.insertItem(i+1, item)
1323                item.setSelected(1)
1324
1325
1326if __name__=="__main__":
1327    import sys
1328    app = QApplication(sys.argv)
1329    #dlg = saveApplicationDlg(None)
1330    dlg = AboutDlg(None)
1331    dlg.show()
1332    app.exec_()
Note: See TracBrowser for help on using the repository browser.