source: orange/Orange/OrangeWidgets/OWBaseWidget.py @ 11746:a11c7c32494a

Revision 11746:a11c7c32494a, 48.0 KB checked in by Ales Erjavec <ales.erjavec@…>, 6 months ago (diff)

Make widgets non resizable (if requested) also on non windows platform.

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