source: orange/Orange/OrangeCanvas/orngDlgs.py @ 11654:3c47a792586a

Revision 11654:3c47a792586a, 61.7 KB checked in by Ales Erjavec <ales.erjavec@…>, 8 months ago (diff)

Changed the global 'addons_corrupted' flag to a function.

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        add, remove, upgrade = self.to_install(), self.to_remove(), self.to_upgrade
857
858        def errormessage(title, message, details=None, exc_info=None):
859            box = QMessageBox(QMessageBox.Critical, title, message,
860                              parent=self)
861
862            if details is not None:
863                box.setDetailedText(details)
864            elif exc_info:
865                import traceback
866                if isinstance(exc_info, tuple):
867                    details = traceback.format_exception(*(exc_info + (10,)))
868                else:
869                    details = traceback.format_exc(10)
870                box.setDetailedText(details)
871
872            return box.exec_()
873
874        for name in upgrade:
875            try:
876                self.busy("Upgrading %s ..." % name)
877                self.repaint()
878                Orange.utils.addons.upgrade(name, self.pcb)
879            except subprocess.CalledProcessError, ex:
880                errormessage("Error",
881                             "setup.py script exited with error code %i" \
882                             % ex.returncode,
883                             details=ex.output)
884            except Exception, e:
885                errormessage("Error",
886                             "Problem upgrading add-on %s: %s" % (name, e),
887                             exc_info=True)
888
889        for name in remove:
890            try:
891                self.busy("Uninstalling %s ..." % name)
892                self.repaint()
893                Orange.utils.addons.uninstall(name, self.pcb)
894            except Exception, e:
895                errormessage("Error",
896                             "Problem uninstalling add-on %s: %s" % (name, e),
897                             exc_info=True)
898
899        for name in add:
900            try:
901                self.busy("Installing %s ..." % name)
902                self.repaint()
903                Orange.utils.addons.install(name, self.pcb)
904            except subprocess.CalledProcessError, ex:
905                errormessage("Error",
906                             "setup.py script exited with error code %i" \
907                             % ex.returncode,
908                             details=ex.output)
909
910            except Exception, e:
911                errormessage("Error",
912                             "Problem installing add-on %s: %s" % (name, e),
913                             exc_info=True)
914
915        if len(upgrade) > 0:
916            QMessageBox.warning(self, "Restart Orange", "After upgrading add-ons, it is very important to restart Orange to make sure the changes have been applied.")
917        elif len(remove) > 0:  # Don't bother with this if there has already been one (more important) warning.
918            QMessageBox.warning(self, "Restart Orange", "After removal of add-ons, it is suggested that you restart Orange for the changes to become effective.")
919
920        QDialog.accept(self)
921
922    def busy(self, b=True):
923        self.progress.setMaximum(1)
924        self.progress.setValue(0)
925        self.progress.setVisible(bool(b))
926        self.busyLbl.setText(b if isinstance(b, str) else "")
927        self.eSearch.setEnabled(not b)
928        self.lst.setEnabled(not b)
929        self.okButton.setEnabled(not b)
930        self.cancelButton.setEnabled(not b)
931        self.rightPanel.setEnabled(not b)
932        self.infoPane.setEnabled(not b)
933
934    def pcb(self, max, val):
935        self.progress.setMaximum(max)
936        self.progress.setValue(val)
937        qApp.processEvents(QEventLoop.ExcludeUserInputEvents)
938
939    def reloadRepo(self):
940        # Reload add-on list.
941        try:
942            self.busy("Reloading add-on repository ...")
943            self.repaint()
944            Orange.utils.addons.refresh_available_addons(progress_callback = self.pcb)
945            if self.savetimefn:
946                self.savetimefn(int(time.time()))
947        except Exception, e:
948            QMessageBox.critical(self, "Error", "Could not reload repository: %s." % e)
949        finally:
950            self.busy(False)
951        # Finally, refresh the tree on GUI.
952        self.refreshView()
953
954    def reloadQ(self):
955        #ask the user if he would like to reload the repository
956        lastRefresh = 0
957        if self.loadtimefn:
958            lastRefresh = self.loadtimefn()
959        t = time.time()
960        if t - lastRefresh > 7*24*3600 or Orange.utils.addons.addons_corrupted():
961            if Orange.utils.addons.addons_corrupted() or \
962               QMessageBox.question(self, "Refresh",
963                                    "List of available add-ons has not been refreshed for more than a week. Do you want to download the list now?",
964                                     QMessageBox.Yes | QMessageBox.Default,
965                                     QMessageBox.No | QMessageBox.Escape) == QMessageBox.Yes:
966                self.reloadRepo()
967           
968    def upgradeCandidates(self):
969        result = []
970        import Orange.utils.addons
971        with closing(Orange.utils.addons.open_addons()) as addons:
972            for ao in addons.values():
973                if ao.installed_version and ao.available_version and ao.installed_version != ao.available_version:
974                    result.append(ao.name)
975        return result
976   
977    def upgradeAll(self):
978        for candidate in self.upgradeCandidates():
979            self.upgrade(candidate, refresh=False)
980        self.refreshInfoPane()
981
982    def upgrade(self, name=None, refresh=True):
983        if not name:
984            name = self.getAddOnIdFromItem(self.lst.currentItem())
985        self.to_upgrade.add(name)
986        if refresh:
987            self.refreshInfoPane()
988
989    def openWebPage(self):
990        addon = self.getAddOnFromItem(self.lst.currentItem())
991        if addon and addon.homepage:
992            import webbrowser
993            webbrowser.open(addon.homepage)
994
995    def openDocsPage(self):
996        addon = self.getAddOnFromItem(self.lst.currentItem())
997        if addon and addon.docs_url:
998            import webbrowser
999            webbrowser.open(addon.docs_url)
1000
1001    def listWidgets(self):
1002        addOn = self.getAddOnFromItem(self.lst.currentItem())
1003        if not addOn: return
1004        import Orange.utils.addons
1005        if addOn.__class__ is not Orange.utils.addons.OrangeAddOnInRepo: return
1006        if not addOn.repository.has_web_script: return
1007        self.canvasDlg.helpWindow.open("%s/addOnServer.py/%s/doc/widgets/" % (addOn.repository.url, addOn.filename), modal=True)
1008       
1009       
1010    def donotUpgrade(self):
1011        id = self.getAddOnIdFromItem(self.lst.currentItem())
1012        self.to_upgrade.remove(id)
1013        self.refreshInfoPane()
1014
1015    def cbToggled(self, item):
1016        ao = self.getAddOnFromItem(item)
1017        if ao and not has_pip and ao.installed_version and item.checkState()==Qt.Unchecked:
1018            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.")
1019            item.setCheckState(Qt.Checked)
1020        self.refreshInfoPane(item)
1021
1022    def lineEditSearch(self, *args, **props):
1023        return OWGUI.lineEdit(*args, **props)
1024
1025    def getAddOnFromItem(self, item):
1026        return getattr(item, "addon", None)
1027
1028    def getAddOnIdFromItem(self, item):
1029        addon = self.getAddOnFromItem(item)
1030        return addon.name if addon else None
1031       
1032    def refreshInfoPane(self, item=None):
1033        if not item:
1034            item = self.lst.currentItem()
1035        addon = None
1036        if item:
1037            import Orange.utils.addons
1038            import orngEnviron
1039            addon = self.getAddOnFromItem(item)
1040        if addon:
1041            self.lblDescription.setText((addon.summary.strip() or "") +"\n"+ (addon.description.strip() or ""))
1042            self.lblVerAvailValue.setText(addon.available_version or "")
1043
1044            self.lblVerInstalledValue.setText(addon.installed_version or "-") #TODO Tell whether it's a system-wide installation
1045            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
1046            self.donotUpgradeButton.setVisible(addon.name in self.to_upgrade)
1047            self.webButton.setVisible(bool(addon.homepage))
1048            self.docButton.setVisible(bool(addon.docs_url))
1049            self.listWidgetsButton.setVisible(False) #TODO A list of widgets is not available
1050
1051            if not addon.installed_version and item.checkState()==Qt.Checked:
1052                self.lblStatus.setText("marked for installation")
1053            elif addon.installed_version and item.checkState()!=Qt.Checked:
1054                self.lblStatus.setText("marked for removal")
1055            elif addon.name in self.to_upgrade:
1056                self.lblStatus.setText("marked for upgrade")
1057            else:
1058                self.lblStatus.setText("")
1059
1060            self.infoPane.setVisible(True)
1061        else:
1062            self.infoPane.setVisible(False)
1063        self.enableDisableButtons()
1064
1065    def enableDisableButtons(self):
1066        import Orange.utils.addons
1067        with closing(Orange.utils.addons.open_addons()) as addons:
1068            aos = addons.values()
1069            self.upgradeAllButton.setEnabled(any(ao.installed_version and ao.available_version and
1070                                                 ao.installed_version != ao.available_version and
1071                                                 ao.name not in self.to_upgrade for ao in aos))
1072       
1073    def currentItemChanged(self, new, previous):
1074        # Refresh info pane & button states
1075        self.refreshInfoPane(new)
1076
1077    def addAddOnsToTree(self, addon_dict, selected=None, to_install=[], to_remove=[]):
1078        # Sort alphabetically
1079        addons = sorted(list(addon_dict.items()),
1080                        key = lambda (name, ao): name)
1081
1082        for (i, (name, ao)) in enumerate(addons):
1083            item = QListWidgetItem()
1084            self.lst.addItem(item)
1085            item.setText(ao.name)
1086            item.setCheckState(Qt.Checked if ao.installed_version and not name in to_remove or name in to_install else Qt.Unchecked)
1087            item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsSelectable | Qt.ItemIsEnabled)
1088            item.addon = ao
1089            if name == selected:
1090                self.lst.setCurrentItem(item)
1091
1092            item.disableToggleSignal = False
1093
1094    def lst_items(self):
1095        for i in xrange(self.lst.count()):
1096            yield self.lst.item(i)
1097
1098    def to_install(self):
1099        return set([item.addon.name for item in self.lst_items()
1100                    if item.checkState()==Qt.Checked and not item.addon.installed_version])
1101
1102    def to_remove(self):
1103        return set([item.addon.name for item in self.lst_items()
1104                    if item.checkState()!=Qt.Checked and item.addon.installed_version])
1105
1106    def refreshView(self, selectedRegisteredAddOnId=None):
1107        import Orange
1108        # Save current item selection
1109        selected_addon = self.getAddOnIdFromItem(self.lst.currentItem())
1110        to_install = self.to_install()
1111        to_remove = self.to_remove()
1112        #TODO: Save the next repository selection too, in case the current one was deleted
1113
1114        # Clear the tree
1115        self.lst.clear()
1116       
1117        # Add repositories and add-ons
1118        with closing(Orange.utils.addons.open_addons()) as global_addons:
1119            addons = {}
1120            for name in Orange.utils.addons.search_index(self.searchStr):
1121                addons[name.lower()] = global_addons[name.lower()]
1122            self.addAddOnsToTree(addons, selected = selected_addon, to_install=to_install, to_remove=to_remove)
1123            self.refreshInfoPane()
1124
1125        #TODO Should we somehow show the legacy registered addons?
1126
1127    def searchCallback(self):
1128        self.refreshView()
1129   
1130# widget shortcuts dialog
1131class WidgetShortcutDlg(QDialog):
1132    def __init__(self, canvasDlg, *args):
1133        import orngTabs
1134
1135        apply(QDialog.__init__,(self,) + args)
1136        self.canvasDlg = canvasDlg
1137        self.setWindowTitle("Widget Shortcuts")
1138        self.setLayout(QVBoxLayout())
1139        self.layout().setSpacing(10)
1140        self.resize(700,500)
1141
1142        self.invDict = dict([(y, x) for x, y in canvasDlg.widgetShortcuts.items()])
1143        invInvDict = {}
1144
1145        self.tabs = QTabWidget(self)
1146       
1147        extraTabs = [(name, 1) for name in canvasDlg.widgetRegistry.keys() if name not in [tab for (tab, s) in canvasDlg.settings["WidgetTabs"]]]
1148        for tabName, show in canvasDlg.settings["WidgetTabs"] + extraTabs:
1149            if not canvasDlg.widgetRegistry.has_key(tabName):
1150                continue
1151            scrollArea = QScrollArea()
1152            self.tabs.addTab(scrollArea, tabName)
1153            #scrollArea.setWidgetResizable(1)       # you have to use this or set size to wtab manually - otherwise nothing gets shown
1154
1155            wtab = QWidget(self.tabs)
1156            scrollArea.setWidget(wtab)
1157
1158            widgets = [(int(widgetInfo.priority), name, widgetInfo) for (name, widgetInfo) in canvasDlg.widgetRegistry[tabName].items()]
1159            widgets.sort()
1160            rows = (len(widgets)+2) / 3
1161            layout = QGridLayout(wtab)
1162
1163            for i, (priority, name, widgetInfo) in enumerate(widgets):
1164                x = i / rows
1165                y = i % rows
1166
1167                hlayout = QHBoxLayout()
1168                mainBox = QWidget(wtab)
1169                mainBox.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed))
1170                mainBox.setLayout(hlayout)
1171                layout.addWidget(mainBox, y, x, Qt.AlignTop | Qt.AlignLeft)
1172                label = QLabel(wtab)
1173                label.setPixmap(canvasDlg.getWidgetIcon(widgetInfo).pixmap(40))
1174                hlayout.addWidget(label)
1175
1176                optionsw = QWidget(self)
1177                optionsw.setLayout(QVBoxLayout())
1178                hlayout.addWidget(optionsw)
1179                optionsw.layout().addStretch(1)
1180
1181                OWGUI.widgetLabel(optionsw, name)
1182                key = self.invDict.get(widgetInfo, "<none>")
1183                le = KeyEdit(optionsw, key, self.invDict, widgetInfo, invInvDict)
1184                optionsw.layout().addWidget(le)
1185                invInvDict[key] = le
1186                le.setFixedWidth(60)
1187
1188            wtab.resize(wtab.sizeHint())
1189
1190        # OK, Cancel buttons
1191        hbox = OWGUI.widgetBox(self, orientation = "horizontal", sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed))
1192        hbox.layout().addStretch(1)
1193        self.okButton = OWGUI.button(hbox, self, "OK", callback = self.accept)
1194        self.cancelButton = OWGUI.button(hbox, self, "Cancel", callback = self.reject)
1195        self.okButton.setDefault(True)
1196
1197        self.layout().addWidget(self.tabs)
1198        self.layout().addWidget(hbox)
1199
1200
1201class AboutDlg(QDialog):
1202    def __init__(self, *args):
1203        QDialog.__init__(self, *args)
1204        self.topLayout = QVBoxLayout(self)
1205#        self.setWindowFlags(Qt.Popup)       # Commented out, because it makes the window appear in the top-left corner on Linux
1206        self.setWindowTitle("About Orange")
1207       
1208        import orngEnviron
1209        import Orange
1210        import re
1211        logoImage = QPixmap(os.path.join(orngEnviron.directoryNames["canvasDir"], "icons", "splash.png"))
1212        logo = OWGUI.widgetLabel(self, "")
1213        logo.setPixmap(logoImage)
1214       
1215        OWGUI.widgetLabel(self, '<p align="center"><h2>Orange</h2></p>') 
1216       
1217        default_version_str = Orange.__version__
1218        built_on = re.findall("\((.*?)\)", Orange.orange.version)
1219        if built_on:
1220            built_on_str = " (built on " + built_on[0].split(",")[-1] + ")"
1221        else:
1222            built_on_str = ""
1223        try:
1224            import Orange.version as version
1225            short_version = version.short_version
1226            hg_revision = version.hg_revision
1227            OWGUI.widgetLabel(self, '<p align="center">version %s</p>' % (short_version + built_on_str))
1228            if not version.release:
1229                OWGUI.widgetLabel(self, '<p align="center">(hg revision %s)</p>' % (hg_revision))
1230        except ImportError:
1231            OWGUI.widgetLabel(self, '<p align="center">version %s</p>' % (default_version_str + built_on_str))
1232        OWGUI.widgetLabel(self, "" )
1233        #OWGUI.button(self, self, "Close", callback = self.accept)
1234        b = QDialogButtonBox(self)
1235        b.setCenterButtons(1)
1236        self.layout().addWidget(b)
1237        butt = b.addButton(QDialogButtonBox.Close)
1238        self.connect(butt, SIGNAL("clicked()"), self.accept)
1239       
1240
1241class saveApplicationDlg(QDialog):
1242    def __init__(self, *args):
1243        import Orange.utils.addons
1244       
1245        apply(QDialog.__init__,(self,) + args)
1246        self.setWindowTitle("Set Widget Order")
1247        self.setLayout(QVBoxLayout())
1248
1249        listbox = OWGUI.widgetBox(self, 1, orientation = "horizontal", sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum))
1250        self.tabOrderList = QListWidget(listbox)
1251        self.tabOrderList.setSelectionMode(QListWidget.SingleSelection)
1252        listbox.layout().addWidget(self.tabOrderList)
1253
1254        w = OWGUI.widgetBox(listbox, sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding))
1255        self.upButton = OWGUI.button(w, self, "Up", callback = self.moveUp)
1256        self.downButton = OWGUI.button(w, self, "Down", callback = self.moveDown)
1257        w.layout().addStretch(1)
1258
1259        hbox = OWGUI.widgetBox(self, orientation = "horizontal", sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed))
1260        OWGUI.button(hbox, self, "Add Separator", callback = self.insertSeparator)
1261        hbox.layout().addStretch(1)
1262        OWGUI.button(hbox, self, "&OK", callback = self.accept)
1263        OWGUI.button(hbox, self, "&Cancel", callback = self.reject)
1264
1265        self.resize(200,250)
1266
1267    def accept(self):
1268        self.shownWidgetList = [str(self.tabOrderList.item(i).text()) for i in range(self.tabOrderList.count())]
1269        QDialog.accept(self)
1270
1271    def insertSeparator(self):
1272        curr = self.tabOrderList.indexFromItem(self.tabOrderList.currentItem()).row()
1273        self.insertWidgetName("[Separator]", curr)
1274
1275    def insertWidgetName(self, name, index = -1):
1276        if index == -1:
1277            self.tabOrderList.addItem(name)
1278        else:
1279            self.tabOrderList.insertItem(index, name)
1280
1281    # move selected widget category up
1282    def moveUp(self):
1283        for i in range(1, self.tabOrderList.count()):
1284            if self.tabOrderList.item(i).isSelected():
1285                item = self.tabOrderList.takeItem(i)
1286                for j in range(self.tabOrderList.count()): self.tabOrderList.item(j).setSelected(0)
1287                self.tabOrderList.insertItem(i-1, item)
1288                item.setSelected(1)
1289
1290
1291    # move selected widget category down
1292    def moveDown(self):
1293        for i in range(self.tabOrderList.count()-2,-1,-1):
1294            if self.tabOrderList.item(i).isSelected():
1295                item = self.tabOrderList.takeItem(i)
1296                for j in range(self.tabOrderList.count()): self.tabOrderList.item(j).setSelected(0)
1297                self.tabOrderList.insertItem(i+1, item)
1298                item.setSelected(1)
1299
1300
1301if __name__=="__main__":
1302    import sys
1303    app = QApplication(sys.argv)
1304    #dlg = saveApplicationDlg(None)
1305    dlg = AboutDlg(None)
1306    dlg.show()
1307    app.exec_()
Note: See TracBrowser for help on using the repository browser.