source: orange/Orange/OrangeWidgets/OWBaseWidget.py @ 11548:635226cda157

Revision 11548:635226cda157, 46.9 KB checked in by Ales Erjavec <ales.erjavec@…>, 11 months ago (diff)

Do not overwrite saved geometry in closeEvent if the widget was already hidden.

On some platforms (GNOME) the widget's position is reset to the default after
being hidden.

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