source: orange/Orange/OrangeWidgets/OWBaseWidget.py @ 11592:53ab0451fc03

Revision 11592:53ab0451fc03, 47.4 KB checked in by Ales Erjavec <ales.erjavec@…>, 10 months ago (diff)

Added progress bar state tracking in OWBaseWidget.

Line 
1#
2# OWWidget.py
3# Orange Widget
4# A General Orange Widget, from which all the Orange Widgets are derived
5#
6import warnings
7
8from Orange.utils import environ
9from Orange.orng.orngEnviron import directoryNames as old_directory_names
10from PyQt4.QtCore import *
11from PyQt4.QtGui import *
12
13# Define  pyqtConfigure not available in PyQt4 versions prior to 4.6
14if not hasattr(QObject, "pyqtConfigure"):
15    def pyqtConfigure(obj, **kwargs):
16        meta = obj.metaObject()
17        for name, val in kwargs.items():
18            if meta.indexOfProperty(name) >= 0:
19                obj.setProperty(name, QVariant(val))
20            elif meta.indexOfSignal(meta.normalizedSignature(name)) >= 0:
21                obj.connect(obj, SIGNAL(name), val)
22    QObject.pyqtConfigure = pyqtConfigure
23
24from OWContexts import *
25import sys, time, random, user, os, os.path, cPickle, copy
26import logging
27
28import orange
29from Orange import misc
30import Orange.utils
31from Orange.utils import debugging as orngDebugging
32from string import *
33
34from Orange.OrangeCanvas.registry.description import (
35    Default, NonDefault, Single, Multiple, Explicit, Dynamic,
36    InputSignal, OutputSignal
37)
38
39from Orange.OrangeCanvas.scheme.widgetsscheme import (
40    SignalLink, WidgetsSignalManager, SignalWrapper
41)
42
43import OWGUI
44
45
46_log = logging.getLogger(__name__)
47
48ERROR = 0
49WARNING = 1
50
51TRUE = 1
52FALSE = 0
53
54
55def unisetattr(self, name, value, grandparent):
56    if "." in name:
57        names = name.split(".")
58        lastname = names.pop()
59        obj = reduce(lambda o, n: getattr(o, n, None),  names, self)
60    else:
61        lastname, obj = name, self
62
63    if not obj:
64        print "unable to set setting ", name, " to value ", value
65    else:
66        if hasattr(grandparent, "__setattr__") and isinstance(obj, grandparent):
67            grandparent.__setattr__(obj, lastname,  value)
68        else:
69            setattr(obj, lastname, value)
70#            obj.__dict__[lastname] = value
71
72    controlledAttributes = hasattr(self, "controlledAttributes") and getattr(self, "controlledAttributes", None)
73    controlCallback = controlledAttributes and controlledAttributes.get(name, None)
74    if controlCallback:
75        for callback in controlCallback:
76            callback(value)
77#        controlCallback(value)
78
79    # controlled things (checkboxes...) never have __attributeControllers
80    else:
81        if hasattr(self, "__attributeControllers"):
82            for controller, myself in self.__attributeControllers.keys():
83                if getattr(controller, myself, None) != self:
84                    del self.__attributeControllers[(controller, myself)]
85                    continue
86
87                controlledAttributes = hasattr(controller, "controlledAttributes") and getattr(controller, "controlledAttributes", None)
88                if controlledAttributes:
89                    fullName = myself + "." + name
90
91                    controlCallback = controlledAttributes.get(fullName, None)
92                    if controlCallback:
93                        for callback in controlCallback:
94                            callback(value)
95
96                    else:
97                        lname = fullName + "."
98                        dlen = len(lname)
99                        for controlled in controlledAttributes.keys():
100                            if controlled[:dlen] == lname:
101                                self.setControllers(value, controlled[dlen:], controller, fullName)
102                                # no break -- can have a.b.c.d and a.e.f.g; needs to set controller for all!
103
104
105    # if there are any context handlers, call the fastsave to write the value into the context
106    if hasattr(self, "contextHandlers") and hasattr(self, "currentContexts"):
107        for contextName, contextHandler in self.contextHandlers.items():
108            contextHandler.fastSave(self.currentContexts.get(contextName), self, name, value)
109
110
111
112class ControlledAttributesDict(dict):
113    def __init__(self, master):
114        self.master = master
115
116    def __setitem__(self, key, value):
117        if not self.has_key(key):
118            dict.__setitem__(self, key, [value])
119        else:
120            dict.__getitem__(self, key).append(value)
121        self.master.setControllers(self.master, key, self.master, "")
122
123
124from orange import ExampleTable
125
126class AttributeList(list):
127    pass
128
129class ExampleList(list):
130    pass
131
132widgetId = 0
133
134class OWBaseWidget(QDialog):
135    def __new__(cls, *arg, **args):
136        self = QDialog.__new__(cls)
137
138        self.currentContexts = {}   # the "currentContexts" MUST be the first thing assigned to a widget
139        self._useContexts = 1       # do you want to use contexts
140        self._owInfo = 1            # currently disabled !!!
141        self._owWarning = 1         # do we want to see warnings
142        self._owError = 1           # do we want to see errors
143        self._owShowStatus = 0      # do we want to see warnings and errors in status bar area of the widget
144        self._guiElements = []      # used for automatic widget debugging
145        for key in args:
146            if key in ["_owInfo", "_owWarning", "_owError", "_owShowStatus", "_useContexts", "_category", "_settingsFromSchema"]:
147                self.__dict__[key] = args[key]        # we cannot use __dict__.update(args) since we can have many other
148
149        return self
150
151
152    def __init__(self, parent = None, signalManager = None, title="Orange BaseWidget", modal=FALSE, savePosition = False, resizingEnabled = 1, **args):
153        if resizingEnabled:
154            QDialog.__init__(self, parent, Qt.Window)
155        else:
156            QDialog.__init__(self, parent, Qt.Dialog | Qt.MSWindowsFixedSizeDialogHint)# | Qt.WindowMinimizeButtonHint)
157           
158        # do we want to save widget position and restore it on next load
159        self.savePosition = savePosition
160        if savePosition:
161            self.settingsList = getattr(self, "settingsList", []) + ["widgetShown", "savedWidgetGeometry"]
162
163        # directories are better defined this way, otherwise .ini files get written in many places
164        self.__dict__.update(old_directory_names)
165        try:
166            self.__dict__["thisWidgetDir"] = os.path.dirname(sys.modules[self.__class__.__module__].__file__)
167        except:
168            pass
169
170        self.setCaption(title.replace("&","")) # used for widget caption
171        self.setFocusPolicy(Qt.StrongFocus)
172
173        # number of control signals, that are currently being processed
174        # needed by signalWrapper to know when everything was sent
175        self.parent = parent
176        self.needProcessing = 0     # used by signalManager
177
178        self.signalManager = signalManager
179
180        self.inputs = []     # signalName:(dataType, handler, onlySingleConnection)
181        self.outputs = []    # signalName: dataType
182        self.wrappers = []    # stored wrappers for widget events
183        self.linksIn = {}      # signalName : (dirty, widgetFrom, handler, signalData)
184        self.linksOut = {}       # signalName: (signalData, id)
185        self.connections = {}   # dictionary where keys are (control, signal) and values are wrapper instances. Used in connect/disconnect
186        self.controlledAttributes = ControlledAttributesDict(self)
187        self.progressBarHandler = None  # handler for progress bar events
188        self.processingHandler = None   # handler for processing events
189        self.eventHandler = None
190        self.callbackDeposit = []
191        self.startTime = time.time()    # used in progressbar
192
193        self.widgetStateHandler = None
194        self.widgetState = {"Info":{}, "Warning":{}, "Error":{}}
195
196        if hasattr(self, "contextHandlers"):
197            for contextHandler in self.contextHandlers.values():
198                contextHandler.initLocalContext(self)
199               
200        global widgetId
201        widgetId += 1
202        self.widgetId = widgetId
203
204        self.asyncCalls = []
205        self.asyncBlock = False
206        self.__wasShown = False
207        self.__progressBarValue = -1
208        self.__progressState = 0
209
210    # uncomment this when you need to see which events occured
211    """
212    def event(self, e):
213        #eventDict = dict([(0, 'None'), (1, 'Timer'), (2, 'MouseButtonPress'), (3, 'MouseButtonRelease'), (4, 'MouseButtonDblClick'), (5, 'MouseMove'), (6, 'KeyPress'), (7, 'KeyRelease'), (8, 'FocusIn'), (9, 'FocusOut'), (10, 'Enter'), (11, 'Leave'), (12, 'Paint'), (13, 'Move'), (14, 'Resize'), (15, 'Create'), (16, 'Destroy'), (17, 'Show'), (18, 'Hide'), (19, 'Close'), (20, 'Quit'), (21, 'Reparent'), (22, 'ShowMinimized'), (23, 'ShowNormal'), (24, 'WindowActivate'), (25, 'WindowDeactivate'), (26, 'ShowToParent'), (27, 'HideToParent'), (28, 'ShowMaximized'), (30, 'Accel'), (31, 'Wheel'), (32, 'AccelAvailable'), (33, 'CaptionChange'), (34, 'IconChange'), (35, 'ParentFontChange'), (36, 'ApplicationFontChange'), (37, 'ParentPaletteChange'), (38, 'ApplicationPaletteChange'), (40, 'Clipboard'), (42, 'Speech'), (50, 'SockAct'), (51, 'AccelOverride'), (60, 'DragEnter'), (61, 'DragMove'), (62, 'DragLeave'), (63, 'Drop'), (64, 'DragResponse'), (70, 'ChildInserted'), (71, 'ChildRemoved'), (72, 'LayoutHint'), (73, 'ShowWindowRequest'), (80, 'ActivateControl'), (81, 'DeactivateControl'), (1000, 'User')])
214        eventDict = dict([(0, "None"), (130, "AccessibilityDescription"), (119, "AccessibilityHelp"), (86, "AccessibilityPrepare"), (114, "ActionAdded"), (113, "ActionChanged"), (115, "ActionRemoved"), (99, "ActivationChange"), (121, "ApplicationActivated"), (122, "ApplicationDeactivated"), (36, "ApplicationFontChange"), (37, "ApplicationLayoutDirectionChange"), (38, "ApplicationPaletteChange"), (35, "ApplicationWindowIconChange"), (68, "ChildAdded"), (69, "ChildPolished"), (71, "ChildRemoved"), (40, "Clipboard"), (19, "Close"), (82, "ContextMenu"), (52, "DeferredDelete"), (60, "DragEnter"), (62, "DragLeave"), (61, "DragMove"), (63, "Drop"), (98, "EnabledChange"), (10, "Enter"), (150, "EnterEditFocus"), (124, "EnterWhatsThisMode"), (116, "FileOpen"), (8, "FocusIn"), (9, "FocusOut"), (97, "FontChange"), (159, "GraphicsSceneContextMenu"), (164, "GraphicsSceneDragEnter"), (166, "GraphicsSceneDragLeave"), (165, "GraphicsSceneDragMove"), (167, "GraphicsSceneDrop"), (163, "GraphicsSceneHelp"), (160, "GraphicsSceneHoverEnter"), (162, "GraphicsSceneHoverLeave"), (161, "GraphicsSceneHoverMove"), (158, "GraphicsSceneMouseDoubleClick"), (155, "GraphicsSceneMouseMove"), (156, "GraphicsSceneMousePress"), (157, "GraphicsSceneMouseRelease"), (168, "GraphicsSceneWheel"), (18, "Hide"), (27, "HideToParent"), (127, "HoverEnter"), (128, "HoverLeave"), (129, "HoverMove"), (96, "IconDrag"), (101, "IconTextChange"), (83, "InputMethod"), (6, "KeyPress"), (7, "KeyRelease"), (89, "LanguageChange"), (90, "LayoutDirectionChange"), (76, "LayoutRequest"), (11, "Leave"), (151, "LeaveEditFocus"), (125, "LeaveWhatsThisMode"), (88, "LocaleChange"), (153, "MenubarUpdated"), (43, "MetaCall"), (102, "ModifiedChange"), (4, "MouseButtonDblClick"), (2, "MouseButtonPress"), (3, "MouseButtonRelease"), (5, "MouseMove"), (109, "MouseTrackingChange"), (13, "Move"), (12, "Paint"), (39, "PaletteChange"), (131, "ParentAboutToChange"), (21, "ParentChange"), (75, "Polish"), (74, "PolishRequest"), (123, "QueryWhatsThis"), (14, "Resize"), (117, "Shortcut"), (51, "ShortcutOverride"), (17, "Show"), (26, "ShowToParent"), (50, "SockAct"), (112, "StatusTip"), (100, "StyleChange"), (87, "TabletMove"), (92, "TabletPress"), (93, "TabletRelease"), (171, "TabletEnterProximity"), (172, "TabletLeaveProximity"), (1, "Timer"), (120, "ToolBarChange"), (110, "ToolTip"), (78, "UpdateLater"), (77, "UpdateRequest"), (111, "WhatsThis"), (118, "WhatsThisClicked"), (31, "Wheel"), (132, "WinEventAct"), (24, "WindowActivate"), (103, "WindowBlocked"), (25, "WindowDeactivate"), (34, "WindowIconChange"), (105, "WindowStateChange"), (33, "WindowTitleChange"), (104, "WindowUnblocked"), (126, "ZOrderChange"), (169, "KeyboardLayoutChange"), (170, "DynamicPropertyChange")])
215        if eventDict.has_key(e.type()):
216            print str(self.windowTitle()), eventDict[e.type()]
217        return QDialog.event(self, e)
218    """
219
220    def getIconNames(self, iconName):
221        if type(iconName) == list:      # if canvas sent us a prepared list of valid names, just return those
222            return iconName
223       
224        names = []
225        name, ext = os.path.splitext(iconName)
226        for num in [16, 32, 42, 60]:
227            names.append("%s_%d%s" % (name, num, ext))
228        fullPaths = []
229        for paths in [(self.widgetDir, name), (self.widgetDir, "icons", name), (os.path.dirname(sys.modules[self.__module__].__file__), "icons", name)]:
230            for name in names + [iconName]:
231                fname = os.path.join(*paths)
232                if os.path.exists(fname):
233                    fullPaths.append(fname)
234            if fullPaths != []:
235                break
236
237        if len(fullPaths) > 1 and fullPaths[-1].endswith(iconName):
238            fullPaths.pop()     # if we have the new icons we can remove the default icon
239        return fullPaths
240   
241
242    def setWidgetIcon(self, iconName):
243        iconNames = self.getIconNames(iconName)
244           
245        icon = QIcon()
246        for name in iconNames:
247            pix = QPixmap(name)
248            icon.addPixmap(pix)
249#            frame = QPixmap(os.path.join(self.widgetDir, "icons/frame.png"))
250#            icon = QPixmap(iconName)
251#            result = QPixmap(icon.size())
252#            painter = QPainter()
253#            painter.begin(result)
254#            painter.drawPixmap(0,0, frame)
255#            painter.drawPixmap(0,0, icon)
256#            painter.end()
257
258        self.setWindowIcon(icon)
259       
260
261    # ##############################################
262    def createAttributeIconDict(self):
263        return OWGUI.getAttributeIcons()
264
265    def isDataWithClass(self, data, wantedVarType = None, checkMissing=False):
266        self.error([1234, 1235, 1236])
267        if not data:
268            return 0
269        if not data.domain.classVar:
270            self.error(1234, "A data set with a class attribute is required.")
271            return 0
272        if wantedVarType and data.domain.classVar.varType != wantedVarType:
273            self.error(1235, "Unable to handle %s class." % (data.domain.classVar.varType == orange.VarTypes.Discrete and "discrete" or "continuous"))
274            return 0
275        if checkMissing and not orange.Preprocessor_dropMissingClasses(data):
276            self.error(1236, "Unable to handle data set with no known classes")
277            return 0
278        return 1
279
280    # call processEvents(), but first remember position and size of widget in case one of the events would be move or resize
281    # call this function if needed in __init__ of the widget
282    def safeProcessEvents(self):
283        keys = ["widgetShown"]
284        vals = [(key, getattr(self, key, None)) for key in keys]
285        qApp.processEvents()
286        for (key, val) in vals:
287            if val != None:
288                setattr(self, key, val)
289
290
291    # this function is called at the end of the widget's __init__ when the widgets is saving its position and size parameters
292    def restoreWidgetPosition(self):
293        if self.savePosition:
294            geometry = getattr(self, "savedWidgetGeometry", None)
295            restored = False
296            if geometry is not None:
297               restored =  self.restoreGeometry(QByteArray(geometry))
298               
299            if restored:
300                space = qApp.desktop().availableGeometry(self)
301                frame, geometry = self.frameGeometry(), self.geometry()
302               
303                #Fix the widget size to fit inside the available space
304                width = min(space.width() - (frame.width() - geometry.width()), geometry.width())
305                height = min(space.height() - (frame.height() - geometry.height()), geometry.height())
306                self.resize(width, height)
307               
308                #Move the widget to the center of available space if it is currently outside it
309                if not space.contains(self.frameGeometry()):
310                    x = max(0, space.width() / 2 - width / 2)
311                    y = max(0, space.height() / 2 - height / 2)
312           
313                    self.move(x, y)
314           
315#            geometry.move(frameOffset) #Make sure the title bar is shown
316#            self.setGeometry(geometry.intersected(space.adjusted(-frameOffset.x(), -frameOffset.y(), 0, 0)))
317           
318           
319#            if self.isWindow():
320#                frame = self.frameGeometry()
321#                if space.topLeft() != QPoint(0, 0):
322#                    self.move(self.geometry().topLeft() - frame.topLeft())
323#            if getattr(self, "widgetXPosition", None) != None and getattr(self, "widgetYPosition", None) != None:
324##                print self.captionTitle, "restoring position", self.widgetXPosition, self.widgetYPosition, "to", max(self.widgetXPosition, 0), max(self.widgetYPosition, 0)
325#                self.move(max(self.widgetXPosition, space.x()), max(self.widgetYPosition, space.y()))
326#            if getattr(self,"widgetWidth", None) != None and getattr(self,"widgetHeight", None) != None:
327#                self.resize(min(self.widgetWidth, space.width()), min(self.widgetHeight, space.height()))
328#            frame = self.frameGeometry()
329#            area = lambda rect: rect.width() * rect.height()
330#            if area(frame.intersected(space)) < area(frame):
331#                self.move(max(min(space.right() - frame.width(), frame.x()), space.x()),
332#                          max(min(space.height() - frame.height(), frame.y()), space.y()))
333
334    # this is called in canvas when loading a schema. it opens the widgets that were shown when saving the schema
335    def restoreWidgetStatus(self):
336        if self.savePosition and getattr(self, "widgetShown", None):
337            self.show()
338
339    def resizeEvent(self, ev):
340        QDialog.resizeEvent(self, ev)
341        # Don't store geometry if the widget is not visible
342        # (the widget receives the resizeEvent before showEvent and we must not
343        # overwrite the the savedGeometry before then)
344        if self.savePosition and self.isVisible():
345            self.savedWidgetGeometry = str(self.saveGeometry())
346
347    def showEvent(self, ev):
348        QDialog.showEvent(self, ev)
349        if self.savePosition:
350            self.widgetShown = 1
351
352            if not self.__wasShown:
353                self.__wasShown = True
354                self.restoreWidgetPosition()
355
356    def hideEvent(self, ev):
357        if self.savePosition:
358            self.widgetShown = 0
359            self.savedWidgetGeometry = str(self.saveGeometry())
360        QDialog.hideEvent(self, ev)
361
362    def closeEvent(self, ev):
363        if self.savePosition and self.isVisible() and self.__wasShown:
364            # self.geometry() is 'invalid' (not yet resized/layout) until the
365            # widget is made explicitly visible or it might be invalid if
366            # widget was hidden (in this case hideEvent already saved a valid
367            # geometry).
368            self.savedWidgetGeometry = str(self.saveGeometry())
369        QDialog.closeEvent(self, ev)
370
371    def wheelEvent(self, event):
372        """ Silently accept the wheel event. This is to ensure combo boxes
373        and other controls that have focus don't receive this event unless
374        the cursor is over them.
375       
376        """
377        event.accept()
378
379    def setCaption(self, caption):
380        if self.parent != None and isinstance(self.parent, QTabWidget):
381            self.parent.setTabText(self.parent.indexOf(self), caption)
382        else:
383            # we have to save caption title in case progressbar will change it
384            self.captionTitle = unicode(caption)
385            self.setWindowTitle(caption)
386
387    # put this widget on top of all windows
388    def reshow(self):
389        self.show()
390        self.raise_()
391        self.activateWindow()
392
393
394    def send(self, signalName, value, id = None):
395        if self.linksOut.has_key(signalName):
396            self.linksOut[signalName][id] = value
397        else:
398            self.linksOut[signalName] = {id:value}
399
400        if self.signalManager is not None:
401            self.signalManager.send(self, signalName, value, id)
402
403    def getdeepattr(self, attr, **argkw):
404        try:
405            return reduce(lambda o, n: getattr(o, n, None),  attr.split("."), self)
406        except:
407            if argkw.has_key("default"):
408                return argkw[default]
409            else:
410                raise AttributeError, "'%s' has no attribute '%s'" % (self, attr)
411
412
413    # Set all settings
414    # settings - the map with the settings
415    def setSettings(self,settings):
416        for key in settings:
417            self.__setattr__(key, settings[key])
418        #self.__dict__.update(settings)
419
420    # Get all settings
421    # returns map with all settings
422    def getSettings(self, alsoContexts = True, globalContexts=False):
423        settings = {}
424        if hasattr(self, "settingsList"):
425            for name in self.settingsList:
426                try:
427                    settings[name] =  self.getdeepattr(name)
428                except:
429                    #print "Attribute %s not found in %s widget. Remove it from the settings list." % (name, self.captionTitle)
430                    pass
431       
432        if alsoContexts:
433            self.synchronizeContexts()
434            contextHandlers = getattr(self, "contextHandlers", {})
435            for contextHandler in contextHandlers.values():
436                contextHandler.mergeBack(self)
437#                settings[contextHandler.localContextName] = contextHandler.globalContexts
438# Instead of the above line, I found this; as far as I recall this was a fix
439# for some bugs related to, say, Select Attributes not handling the context
440# attributes properly, but I dare not add it without understanding what it does.
441# Here it is, if these contexts give us any further trouble.
442                if (contextHandler.syncWithGlobal and contextHandler.globalContexts is getattr(self, contextHandler.localContextName)) or globalContexts:
443                    settings[contextHandler.localContextName] = contextHandler.globalContexts
444                else:
445                    contexts = getattr(self, contextHandler.localContextName, None)
446                    if contexts:
447                        settings[contextHandler.localContextName] = contexts
448###
449                settings[contextHandler.localContextName+"Version"] = (contextStructureVersion, contextHandler.contextDataVersion)
450
451        return settings
452
453    def getDefaultSettingsFilename(self):
454        fs_encoding = sys.getfilesystemencoding()
455        basename = self.captionTitle + ".ini"
456        filename = os.path.join(
457            self.widgetSettingsDir,  # is assumed to be a str in FS encoding
458            basename.encode(fs_encoding))
459        return filename
460
461    def getSettingsFile(self, file):
462        if file is None:
463            file = self.getDefaultSettingsFilename()
464
465            if not os.path.exists(file):
466                try:
467                    f = open(file, "wb")
468                    cPickle.dump({}, f)
469                    f.close()
470                except IOError:
471                    return
472        if isinstance(file, basestring):
473            if os.path.exists(file):
474                return open(file, "r")
475        else:
476            return file
477
478
479    # Loads settings from the widget's .ini file
480    def loadSettings(self, file = None):
481        file = self.getSettingsFile(file)
482        if file:
483            try:
484                settings = cPickle.load(file)
485            except Exception, ex:
486                print >> sys.stderr, "Failed to load settings!", repr(ex)
487                settings = None
488           
489            if hasattr(self, "_settingsFromSchema"):
490                if settings: settings.update(self._settingsFromSchema)
491                else:        settings = self._settingsFromSchema
492
493            # can't close everything into one big try-except since this would mask all errors in the below code
494            if settings:
495                if hasattr(self, "settingsList"):
496                    self.setSettings(settings)
497
498                contextHandlers = getattr(self, "contextHandlers", {})
499                for contextHandler in contextHandlers.values():
500                    localName = contextHandler.localContextName
501
502                    structureVersion, dataVersion = settings.get(localName+"Version", (0, 0))
503                    if (structureVersion < contextStructureVersion or dataVersion < contextHandler.contextDataVersion) \
504                       and settings.has_key(localName):
505                        del settings[localName]
506                        delattr(self, localName)
507                        contextHandler.initLocalContext(self)
508                       
509                    if not hasattr(self, "_settingsFromSchema"): #When running stand alone widgets
510                        if contextHandler.syncWithGlobal:
511                            contexts = settings.get(localName, None)
512                            if contexts is not None:
513                                contextHandler.globalContexts = contexts
514                        else:
515                            setattr(self, localName, contextHandler.globalContexts)
516
517
518    def saveSettings(self, file = None):
519        settings = self.getSettings(globalContexts=True)
520        if settings:
521            if file is None:
522                file = self.getDefaultSettingsFilename()
523
524            if isinstance(file, basestring):
525                file = open(file, "w")
526            cPickle.dump(settings, file)
527
528    # Loads settings from string str which is compatible with cPickle
529    def loadSettingsStr(self, str):
530        if str == None or str == "":
531            return
532
533        settings = cPickle.loads(str)
534        self.setSettings(settings)
535
536        contextHandlers = getattr(self, "contextHandlers", {})
537        for contextHandler in contextHandlers.values():
538            localName = contextHandler.localContextName
539            if settings.has_key(localName):
540                structureVersion, dataVersion = settings.get(localName+"Version", (0, 0))
541                if structureVersion < contextStructureVersion or dataVersion < contextHandler.contextDataVersion:
542                    del settings[localName]
543                    delattr(self, localName)
544                    contextHandler.initLocalContext(self)
545                else:
546                    setattr(self, localName, settings[localName])
547
548    # return settings in string format compatible with cPickle
549    def saveSettingsStr(self):
550        settings = self.getSettings()
551        return cPickle.dumps(settings)
552
553    def onDeleteWidget(self):
554        pass
555
556    # this function is only intended for derived classes to send appropriate signals when all settings are loaded
557    def activateLoadedSettings(self):
558        pass
559
560    # reimplemented in other widgets
561    def setOptions(self):
562        pass
563
564    # does widget have a signal with name in inputs
565    def hasInputName(self, name):
566        for input in self.inputs:
567            if name == input[0]: return 1
568        return 0
569
570    # does widget have a signal with name in outputs
571    def hasOutputName(self, name):
572        for output in self.outputs:
573            if name == output[0]: return 1
574        return 0
575
576    def getInputType(self, signalName):
577        for input in self.inputs:
578            if input[0] == signalName: return input[1]
579        return None
580
581    def getOutputType(self, signalName):
582        for output in self.outputs:
583            if output[0] == signalName: return output[1]
584        return None
585
586    # ########################################################################
587    def connect(self, control, signal, method, type=Qt.AutoConnection):
588        wrapper = SignalWrapper(self, method)
589        self.connections[(control, signal)] = wrapper   # save for possible disconnect
590        self.wrappers.append(wrapper)
591        QDialog.connect(control, signal, wrapper, type)
592        #QWidget.connect(control, signal, method)        # ordinary connection useful for dialogs and windows that don't send signals to other widgets
593
594
595    def disconnect(self, control, signal, method=None):
596        wrapper = self.connections[(control, signal)]
597        QDialog.disconnect(control, signal, wrapper)
598
599
600    def getConnectionMethod(self, control, signal):
601        if (control, signal) in self.connections:
602            wrapper = self.connections[(control, signal)]
603            return wrapper.method
604        else:
605            return None
606
607
608    def signalIsOnlySingleConnection(self, signalName):
609        for i in self.inputs:
610            input = InputSignal(*i)
611            if input.name == signalName: return input.single
612
613    def addInputConnection(self, widgetFrom, signalName):
614        for i in range(len(self.inputs)):
615            if self.inputs[i][0] == signalName:
616                handler = self.inputs[i][2]
617                break
618
619        existing = []
620        if self.linksIn.has_key(signalName):
621            existing = self.linksIn[signalName]
622            for (dirty, widget, handler, data) in existing:
623                if widget == widgetFrom: return             # no need to add new tuple, since one from the same widget already exists
624        self.linksIn[signalName] = existing + [(0, widgetFrom, handler, [])]    # (dirty, handler, signalData)
625        #if not self.linksIn.has_key(signalName): self.linksIn[signalName] = [(0, widgetFrom, handler, [])]    # (dirty, handler, signalData)
626
627    # delete a link from widgetFrom and this widget with name signalName
628    def removeInputConnection(self, widgetFrom, signalName):
629        if self.linksIn.has_key(signalName):
630            links = self.linksIn[signalName]
631            for i in range(len(self.linksIn[signalName])):
632                if widgetFrom == self.linksIn[signalName][i][1]:
633                    self.linksIn[signalName].remove(self.linksIn[signalName][i])
634                    if self.linksIn[signalName] == []:  # if key is empty, delete key value
635                        del self.linksIn[signalName]
636                    return
637
638    # return widget, that is already connected to this singlelink signal. If this widget exists, the connection will be deleted (since this is only single connection link)
639    def removeExistingSingleLink(self, signal):
640        for i in self.inputs:
641            input = InputSignal(*i)
642            if input.name == signal and not input.single: return None
643
644        for signalName in self.linksIn.keys():
645            if signalName == signal:
646                widget = self.linksIn[signalName][0][1]
647                del self.linksIn[signalName]
648                return widget
649
650        return None
651
652
653    def handleNewSignals(self):
654        # this is called after all new signals have been handled
655        # implement this in your widget if you want to process something only after you received multiple signals
656        pass
657
658    # signal manager calls this function when all input signals have updated the data
659    def processSignals(self):
660        if self.processingHandler:
661            self.processingHandler(self, 1)    # focus on active widget
662        newSignal = 0        # did we get any new signals
663
664        # we define only a way to handle signals that have defined a handler function
665        for signal in self.inputs:        # we go from the first to the last defined input
666            key = signal[0]
667            if self.linksIn.has_key(key):
668                for i in range(len(self.linksIn[key])):
669                    (dirty, widgetFrom, handler, signalData) = self.linksIn[key][i]
670                    if not (handler and dirty): continue
671                    newSignal = 1
672
673                    qApp.setOverrideCursor(Qt.WaitCursor)
674                    try:
675                        for (value, id, nameFrom) in signalData:
676                            if self.signalIsOnlySingleConnection(key):
677                                self.printEvent("ProcessSignals: Calling %s with %s" % (handler, value), eventVerbosity = 2)
678                                handler(value)
679                            else:
680                                self.printEvent("ProcessSignals: Calling %s with %s (%s, %s)" % (handler, value, nameFrom, id), eventVerbosity = 2)
681                                handler(value, (widgetFrom, nameFrom, id))
682                    except:
683                        type, val, traceback = sys.exc_info()
684                        sys.excepthook(type, val, traceback)  # we pretend that we handled the exception, so that we don't crash other widgets
685                    qApp.restoreOverrideCursor()
686
687                    self.linksIn[key][i] = (0, widgetFrom, handler, []) # clear the dirty flag
688
689        if newSignal == 1:
690            self.handleNewSignals()
691       
692        while self.isBlocking():
693            self.thread().msleep(50)
694            qApp.processEvents()
695
696        if self.processingHandler:
697            self.processingHandler(self, 0)    # remove focus from this widget
698        self.needProcessing = 0
699
700    # set new data from widget widgetFrom for a signal with name signalName
701    def updateNewSignalData(self, widgetFrom, signalName, value, id, signalNameFrom):
702        if not self.linksIn.has_key(signalName): return
703        for i in range(len(self.linksIn[signalName])):
704            (dirty, widget, handler, signalData) = self.linksIn[signalName][i]
705            if widget == widgetFrom:
706                if self.linksIn[signalName][i][3] == []:
707                    self.linksIn[signalName][i] = (1, widget, handler, [(value, id, signalNameFrom)])
708                else:
709                    found = 0
710                    for j in range(len(self.linksIn[signalName][i][3])):
711                        (val, ID, nameFrom) = self.linksIn[signalName][i][3][j]
712                        if ID == id and nameFrom == signalNameFrom:
713                            self.linksIn[signalName][i][3][j] = (value, id, signalNameFrom)
714                            found = 1
715                    if not found:
716                        self.linksIn[signalName][i] = (1, widget, handler, self.linksIn[signalName][i][3] + [(value, id, signalNameFrom)])
717        self.needProcessing = 1
718
719    # ############################################
720    # PROGRESS BAR FUNCTIONS
721
722    progressBarValueChanged = pyqtSignal(float)
723    """Progress bar value has changed"""
724
725    processingStateChanged = pyqtSignal(int)
726    """Processing state has changed"""
727
728    def progressBarInit(self):
729        self.progressBarValue = 0
730        self.startTime = time.time()
731        self.setWindowTitle(self.captionTitle + " (0% complete)")
732        if self.progressBarHandler:
733            self.progressBarHandler(self, 0)
734
735        if self.__progressState != 1:
736            self.__progressState = 1
737            self.processingStateChanged.emit(1)
738
739    def progressBarSet(self, value, processEventsFlags=QEventLoop.AllEvents):
740        """
741        Set the current progress bar to `value`. This method will also call
742        `qApp.processEvents` with the `processEventsFlags` unless the
743        processEventsFlags equals ``None``.
744
745        """
746        old = self.__progressBarValue
747        if value > 0:
748            if self.__progressState != 1:
749                warnings.warn("progressBarSet() called without a "
750                              "preceding progressBarInit()",
751                              stacklevel=2)
752                self.__progressState = 1
753                self.processingStateChanged.emit(1)
754
755            self.__progressBarValue = value
756            usedTime = max(1, time.time() - self.startTime)
757            totalTime = (100.0 * usedTime) / float(value)
758            remainingTime = max(0, totalTime - usedTime)
759            h = int(remainingTime / 3600)
760            min = int((remainingTime - h * 3600) / 60)
761            sec = int(remainingTime - h * 3600 - min * 60)
762            if h > 0:
763                text = "%(h)d:%(min)02d:%(sec)02d" % vars()
764            else:
765                text = "%(min)d:%(sec)02d" % vars()
766            self.setWindowTitle(self.captionTitle + " (%(value).2f%% complete, remaining time: %(text)s)" % vars())
767        else:
768            self.setWindowTitle(self.captionTitle + " (0% complete)")
769        if self.progressBarHandler:
770            self.progressBarHandler(self, value)
771
772        if old != value:
773            self.progressBarValueChanged.emit(value)
774
775        if processEventsFlags is not None:
776            qApp.processEvents(processEventsFlags)
777
778    def progressBarValue(self):
779        return self.__progressBarValue
780
781    progressBarValue = pyqtProperty(
782        float,
783        fset=lambda self, val:
784            OWBaseWidget.progressBarSet(self, val, processEventsFlags=None),
785        fget=progressBarValue
786    )
787
788    def progressBarAdvance(self, value, processEventsFlags=QEventLoop.AllEvents):
789        self.progressBarSet(self.progressBarValue + value, processEventsFlags)
790
791    def progressBarFinished(self):
792        self.setWindowTitle(self.captionTitle)
793        if self.progressBarHandler:
794            self.progressBarHandler(self, 101)
795
796        if self.__progressState != 0:
797            self.__progressState = 0
798            self.processingStateChanged.emit(0)
799
800    # handler must be a function, that receives 2 arguments. First is the widget instance, the second is the value between -1 and 101
801    def setProgressBarHandler(self, handler):
802        self.progressBarHandler = handler
803
804    def setProcessingHandler(self, handler):
805        self.processingHandler = handler
806
807    def setEventHandler(self, handler):
808        self.eventHandler = handler
809
810    def setWidgetStateHandler(self, handler):
811        self.widgetStateHandler = handler
812
813    # if we are in debug mode print the event into the file
814    def printEvent(self, text, eventVerbosity=1):
815        text = self.captionTitle + ": " + text
816
817        if eventVerbosity > 0:
818            _log.debug(text)
819        else:
820            _log.info(text)
821
822        if self.eventHandler:
823            self.eventHandler(text, eventVerbosity)
824
825    def openWidgetHelp(self):
826        if "widgetInfo" in self.__dict__ and \
827                hasattr(qApp, "canvasDlg"):
828            # This widget is on a canvas.
829            qApp.canvasDlg.helpWindow.showHelpFor(self.widgetInfo, True)
830
831    def keyPressEvent(self, e):
832        if e.key() in (Qt.Key_Help, Qt.Key_F1):
833            self.openWidgetHelp()
834        elif (int(e.modifiers()), e.key()) in OWBaseWidget.defaultKeyActions:
835            OWBaseWidget.defaultKeyActions[int(e.modifiers()), e.key()](self)
836        else:
837            QDialog.keyPressEvent(self, e)
838
839    def information(self, id=0, text=None):
840        self.setState("Info", id, text)
841
842    def warning(self, id=0, text=""):
843        self.setState("Warning", id, text)
844
845    def error(self, id=0, text=""):
846        self.setState("Error", id, text)
847
848    def setState(self, stateType, id, text):
849        changed = 0
850        if type(id) == list:
851            for val in id:
852                if self.widgetState[stateType].has_key(val):
853                    self.widgetState[stateType].pop(val)
854                    changed = 1
855        else:
856            if isinstance(id, basestring):
857                # if we call information(), warning(), or error() function
858                # with only one parameter - a string - then set id = 0
859                text = id
860                id = 0
861            if not text:
862                if self.widgetState[stateType].has_key(id):
863                    self.widgetState[stateType].pop(id)
864                    changed = 1
865            else:
866                self.widgetState[stateType][id] = text
867                changed = 1
868
869        if changed:
870            if self.widgetStateHandler:
871                self.widgetStateHandler()
872            elif text: # and stateType != "Info":
873                self.printEvent(stateType + " - " + text)
874           
875            if type(id) == list:
876                for i in id:
877                    self.emit(SIGNAL("widgetStateChanged(QString, int, QString)"),
878                              QString(stateType), i,QString(""))
879            else:
880                self.emit(SIGNAL("widgetStateChanged(QString, int, QString)"),
881                             QString(stateType), id, QString(text or ""))
882            #qApp.processEvents()
883        return changed
884
885    widgetStateChanged = pyqtSignal(QString, int, QString)
886    """Widget state has changed first arg is the state type
887    ('Info', 'Warning' or 'Error') the second is the message id
888    and finally the message string."""
889
890    def widgetStateToHtml(self, info=True, warning=True, error=True):
891        pixmaps = self.getWidgetStateIcons()
892        items = [] 
893        iconPath = {"Info": "canvasIcons:information.png",
894                    "Warning": "canvasIcons:warning.png",
895                    "Error": "canvasIcons:error.png"}
896        for show, what in [(info, "Info"), (warning, "Warning"),(error, "Error")]:
897            if show and self.widgetState[what]:
898                items.append('<img src="%s" style="float: left;"> %s' % (iconPath[what], "\n".join(self.widgetState[what].values())))
899        return "<br>".join(items)
900       
901    @classmethod
902    def getWidgetStateIcons(cls):
903        if not hasattr(cls, "_cached__widget_state_icons"):
904            iconsDir = os.path.join(environ.canvas_install_dir, "icons")
905            QDir.addSearchPath("canvasIcons",os.path.join(environ.canvas_install_dir,
906                "icons/"))
907            info = QPixmap("canvasIcons:information.png")
908            warning = QPixmap("canvasIcons:warning.png")
909            error = QPixmap("canvasIcons:error.png")
910            cls._cached__widget_state_icons = \
911                    {"Info": info, "Warning": warning, "Error": error}
912        return cls._cached__widget_state_icons
913
914    def synchronizeContexts(self):
915        if hasattr(self, "contextHandlers"):
916            for contextName, handler in self.contextHandlers.items():
917                context = self.currentContexts.get(contextName, None)
918                if context:
919                    handler.settingsFromWidget(self, context)
920
921    def openContext(self, contextName="", *arg):
922        if not self._useContexts:
923            return
924        handler = self.contextHandlers[contextName]
925        context = handler.openContext(self, *arg)
926        if context:
927            self.currentContexts[contextName] = context
928
929
930    def closeContext(self, contextName=""):
931        if not self._useContexts:
932            return
933        curcontext = self.currentContexts.get(contextName)
934        if curcontext:
935            self.contextHandlers[contextName].closeContext(self, curcontext)
936            del self.currentContexts[contextName]
937
938    def settingsToWidgetCallback(self, handler, context):
939        pass
940
941    def settingsFromWidgetCallback(self, handler, context):
942        pass
943
944    def setControllers(self, obj, controlledName, controller, prefix):
945        while obj:
946            if prefix:
947#                print "SET CONTROLLERS: %s %s + %s" % (obj.__class__.__name__, prefix, controlledName)
948                if obj.__dict__.has_key("attributeController"):
949                    obj.__dict__["__attributeControllers"][(controller, prefix)] = True
950                else:
951                    obj.__dict__["__attributeControllers"] = {(controller, prefix): True}
952
953            parts = controlledName.split(".", 1)
954            if len(parts) < 2:
955                break
956            obj = getattr(obj, parts[0], None)
957            prefix += parts[0]
958            controlledName = parts[1]
959
960    def __setattr__(self, name, value):
961        return unisetattr(self, name, value, QDialog)
962   
963    defaultKeyActions = {}
964   
965    if sys.platform == "darwin":
966        defaultKeyActions = {
967            (Qt.ControlModifier, Qt.Key_M): lambda self: self.showMaximized if self.isMinimized() else self.showMinimized(),
968            (Qt.ControlModifier, Qt.Key_W): lambda self: self.setVisible(not self.isVisible())}
969
970    def scheduleSignalProcessing(self):
971        """
972        Schedule signal processing by the signal manager.
973
974        ..note:: The processing is already scheduled at the most appropriate
975                 time so you should have few uses for this method.
976        """
977        if self.signalManager is not None:
978            self.signalManager.scheduleSignalProcessing(self)
979
980    def setBlocking(self, state=True):
981        """ Set blocking flag for this widget. While this flag is set this
982        widget and all its descendants will not receive any new signals from
983        the signal manager
984        """
985        self.asyncBlock = state
986        self.emit(SIGNAL("blockingStateChanged(bool)"), self.asyncBlock)
987        if not self.isBlocking():
988            self.scheduleSignalProcessing()
989       
990       
991    def isBlocking(self):
992        """ Is this widget blocking signal processing. Widget is blocking if
993        asyncBlock value is True or any AsyncCall objects in asyncCalls list
994        has blocking flag set
995        """
996        return self.asyncBlock or any(a.blocking for a in self.asyncCalls)
997   
998    def asyncExceptionHandler(self, (etype, value, tb)):
999        import traceback
1000        sys.excepthook(etype, value, tb)
1001       
1002    def asyncFinished(self, async, string):
1003        """ Remove async from asyncCalls, update blocking state
1004        """
1005       
1006        index = self.asyncCalls.index(async)
1007        async = self.asyncCalls.pop(index)
1008       
1009        if async.blocking and not self.isBlocking():
1010            # if we are responsible for unblocking
1011            self.emit(SIGNAL("blockingStateChanged(bool)"), False)
1012            self.scheduleSignalProcessing()
1013           
1014        async.disconnect(async, SIGNAL("finished(PyQt_PyObject, QString)"), self.asyncFinished)
1015        self.emit(SIGNAL("asyncCallsStateChange()"))
1016               
1017           
1018   
1019    def asyncCall(self, func, args=(), kwargs={}, name=None, onResult=None, onStarted=None, onFinished=None, onError=None, blocking=True, thread=None, threadPool=None):
1020        """ Return an OWConcurent.AsyncCall object func, args and kwargs
1021        set and signals connected.
1022        """
1023        from functools import partial
1024        from OWConcurrent import AsyncCall
1025       
1026        asList = lambda slot: slot if isinstance(slot, list) else ([slot] if slot else [])
1027       
1028        onResult = asList(onResult)
1029        onStarted = asList(onStarted) #+ [partial(self.setBlocking, True)]
1030        onFinished = asList(onFinished) #+ [partial(self.blockSignals, False)]
1031        onError = asList(onError) or [self.asyncExceptionHandler]
1032       
1033        async = AsyncCall(func, args, kwargs, thread=thread, threadPool=threadPool)
1034        async.name = name if name is not None else ""
1035           
1036        for slot in  onResult:
1037            async.connect(async, SIGNAL("resultReady(PyQt_PyObject)"), slot, Qt.QueuedConnection)
1038        for slot in onStarted:
1039            async.connect(async, SIGNAL("starting()"), slot, Qt.QueuedConnection)
1040        for slot in onFinished:
1041            async.connect(async, SIGNAL("finished(QString)"), slot, Qt.QueuedConnection)
1042        for slot in onError:
1043            async.connect(async, SIGNAL("unhandledException(PyQt_PyObject)"), slot, Qt.QueuedConnection)
1044       
1045        self.addAsyncCall(async, blocking)
1046           
1047        return async
1048   
1049    def addAsyncCall(self, async, blocking=True):
1050        """ Add AsyncCall object to asyncCalls list (will be removed
1051        once it finishes processing).
1052       
1053        """
1054        ## TODO: make this thread safe
1055       
1056        async.connect(async, SIGNAL("finished(PyQt_PyObject, QString)"), self.asyncFinished)
1057       
1058        async.blocking = blocking
1059       
1060        if blocking:
1061            # if we are responsible for blocking
1062            state = any(a.blocking for a in self.asyncCalls)
1063            self.asyncCalls.append(async)
1064            if not state:
1065                self.emit(SIGNAL("blockingStateChanged(bool)"), True)
1066        else:
1067            self.asyncCalls.append(async)
1068           
1069        self.emit(SIGNAL("asyncCallsStateChange()"))
1070       
1071   
1072   
1073def blocking(method):
1074    """ Return method that sets blocking flag while executing
1075    """
1076    from functools import wraps
1077    @wraps(method)
1078    def wrapper(self, *args, **kwargs):
1079        old = self._blocking
1080        self.setBlocking(True)
1081        try:
1082            return method(self, *args, **kwargs)
1083        finally:
1084            self.setBlocking(old)
1085   
1086
1087if __name__ == "__main__":
1088    a=QApplication(sys.argv)
1089    oww=OWBaseWidget(adfaf=1)
1090    oww.show()
1091    a.exec_()
1092    oww.saveSettings()
Note: See TracBrowser for help on using the repository browser.