├── .gitignore ├── dimscape ├── __init__.py ├── .gitignore ├── kaukatcr │ ├── __init__.py │ └── .gitignore ├── ops │ ├── .gitignore │ ├── __init__.py │ ├── operation.py │ ├── newDim.py │ ├── newCell.py │ ├── unlinkCell.py │ └── linkCell.py ├── menus │ ├── .gitignore │ ├── __init__.py │ ├── newDim.py │ └── newCell.py ├── types │ ├── .gitignore │ ├── clone.py │ ├── __init__.py │ ├── system.py │ ├── text.py │ ├── registrar.py │ ├── media.py │ └── cell.py ├── dimview.py ├── log.py ├── vid_test.py ├── jsonBackend.py ├── main_window.ui ├── connection.py ├── windowUI.py └── space.py ├── dimscape.desktop ├── setup.py ├── README └── bin └── dimscape /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | -------------------------------------------------------------------------------- /dimscape/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dimscape/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /dimscape/kaukatcr/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dimscape/ops/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /dimscape/kaukatcr/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /dimscape/menus/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /dimscape/types/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /dimscape/menus/__init__.py: -------------------------------------------------------------------------------- 1 | from newDim import NewDimMenu 2 | from newCell import NewCellMenu 3 | -------------------------------------------------------------------------------- /dimscape/ops/__init__.py: -------------------------------------------------------------------------------- 1 | from newCell import NewCellOperation 2 | from newDim import NewDimOperation 3 | from linkCell import LinkOperation 4 | from unlinkCell import UnlinkCellOperation 5 | -------------------------------------------------------------------------------- /dimscape/types/clone.py: -------------------------------------------------------------------------------- 1 | from __future__ import generators, print_function 2 | # -*- coding: utf-8 -*- 3 | 4 | import cell 5 | 6 | class CloneCell(cell.CellSkin): 7 | pass 8 | 9 | # 10 | -------------------------------------------------------------------------------- /dimscape/dimview.py: -------------------------------------------------------------------------------- 1 | from __future__ import generators, print_function 2 | # -*- coding: utf-8 -*- 3 | 4 | import os,sys 5 | 6 | # Import Qt modules 7 | from PyQt4 import QtGui 8 | 9 | class DimView(QtGui.QGraphicsView): 10 | def __init__(self, parent=None): 11 | QtGui.QGraphicsView.__init__(self, parent) 12 | 13 | def keyPressEvent(self, evt): 14 | QtGui.QWidget.keyPressEvent(self, evt) 15 | -------------------------------------------------------------------------------- /dimscape.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Version=1.0 3 | Name=Multidimensional Database Manipulation Program 4 | GenericName=Database Editor 5 | Comment=Create datasets in many dimensions and edit them 6 | Exec=dimscape %F 7 | TryExec=dimscape 8 | Icon=dimscape 9 | Terminal=false 10 | StartupNotify=true 11 | Categories=Qt;Database;Viewer;DataVisualization; 12 | MimeType=application/x-dimscape; 13 | Type=Application 14 | 15 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from distutils.core import setup 4 | from distutils.extension import Extension 5 | 6 | 7 | setup(name='Dimscape', 8 | version='0.1.0', 9 | description='Multi-dimensional database navigator and editor', 10 | license="GPLv3 or later", 11 | author='Jonathan Kopetz', 12 | author_email='ccs4ever@gmail.com', 13 | url='', 14 | scripts=['bin/dimscape'], 15 | provides=['dimscape'], 16 | requires=['PyQt4'], 17 | packages=['dimscape', 'dimscape.kaukatcr', 'dimscape.types', 'dimscape.menus', 18 | 'dimscape.ops'], 19 | ) 20 | -------------------------------------------------------------------------------- /dimscape/types/__init__.py: -------------------------------------------------------------------------------- 1 | from registrar import CellTypeRegistrar 2 | from media import VideoCell, AudioCell, ImageCell 3 | from text import TextCell 4 | from system import ProgCell, SystemWarnCell 5 | from clone import CloneCell 6 | 7 | # system types get registered here for now, fixes cycle between 8 | # registrar and registrees 9 | CellTypeRegistrar.get().register("video", VideoCell, system=False) 10 | CellTypeRegistrar.get().register("warn", SystemWarnCell, system=True) 11 | CellTypeRegistrar.get().register("prog", ProgCell, system=False) 12 | CellTypeRegistrar.get().register("text", TextCell, system=False) 13 | -------------------------------------------------------------------------------- /dimscape/log.py: -------------------------------------------------------------------------------- 1 | import logging as l 2 | import logging.handlers as h 3 | 4 | initted = False 5 | 6 | def initLog(level=l.DEBUG): 7 | global initted 8 | if not initted: 9 | initted = True 10 | root = l.getLogger() 11 | root.setLevel(level) 12 | errHandler = h.SysLogHandler("/dev/log") 13 | errHandler.setLevel(l.ERROR) 14 | f = l.Formatter("%(name)s [%(module)s/%(funcName)s/%(lineno)d] %(levelname)s: %(message)s") 15 | errHandler.setFormatter(f) 16 | root.addHandler(errHandler) 17 | othHandler = l.StreamHandler() 18 | othHandler.setFormatter(l.Formatter(l.BASIC_FORMAT)) 19 | othHandler.setLevel(level) 20 | root.addHandler(othHandler) 21 | -------------------------------------------------------------------------------- /dimscape/ops/operation.py: -------------------------------------------------------------------------------- 1 | from __future__ import generators, print_function 2 | # -*- coding: utf-8 -*- 3 | 4 | class Operation(object): 5 | 6 | def __init__(self, space, msg_space): 7 | object.__init__(self) 8 | self.space = space 9 | self.msg_space = msg_space 10 | self.needsInput = True 11 | 12 | def show(self): 13 | pass 14 | 15 | def cancel(self): 16 | self.needsInput = False 17 | 18 | def isFinished(self): 19 | return (not self.needsInput) 20 | 21 | def finish(self, msg): 22 | self.report(msg) 23 | self.needsInput = False 24 | 25 | def report(self, msg): 26 | self.msg_space.setPlainText(msg) 27 | 28 | def reportError(self, err, fatal=False): 29 | if fatal: self.finish(err) 30 | else: self.report(err) 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /dimscape/vid_test.py: -------------------------------------------------------------------------------- 1 | from pyffmpeg import * 2 | from PyQt4 import QtGui, QtCore 3 | import numpy 4 | import sys, os 5 | from windowUI import Ui_MainWindow 6 | 7 | class Main(QtGui.QMainWindow): 8 | def __init__(self): 9 | QtGui.QWidget.__init__(self) 10 | 11 | # This is always the same 12 | self.ui=Ui_MainWindow() 13 | self.ui.setupUi(self) 14 | self.cells = [] 15 | 16 | self.scene=QtGui.QGraphicsScene() 17 | self.scene.setSceneRect(0,0,1024,768) 18 | self.ui.dimscapeView.setScene(self.scene) 19 | 20 | app = QtGui.QApplication(sys.argv) 21 | 22 | window = Main() 23 | window.show() 24 | 25 | mp = FFMpegReader() 26 | mp.open("../zzChemDemo-Moore.mov") 27 | mp.seek_to(10) 28 | frame = mp.get_current_frame() 29 | #print (frame) 30 | height = len(frame[0][2]) 31 | width = len(frame[0][2][0]) 32 | #print (width, height) 33 | fbuf = numpy.core.multiarray.getbuffer(frame[0][2]) 34 | img = QtGui.QImage(fbuf, width, height, width*3, QtGui.QImage.Format_RGB888) 35 | #print (img, img.width(), img.height()) 36 | pix = QtGui.QPixmap.fromImage(img) 37 | #print (pix) 38 | gpix = window.scene.addPixmap(pix) 39 | window.ui.dimscapeView.centerOn(gpix) 40 | sys.exit(app.exec_()) 41 | -------------------------------------------------------------------------------- /dimscape/ops/newDim.py: -------------------------------------------------------------------------------- 1 | from __future__ import generators, print_function 2 | # -*- coding: utf-8 -*- 3 | 4 | from PyQt4.QtCore import Qt 5 | 6 | from operation import Operation 7 | from dimscape.menus import NewDimMenu 8 | from dimscape.types.cell import Cell 9 | 10 | class NewDimOperation(Operation): 11 | 12 | def __init__(self, space, attachOrigin, msg_space): 13 | Operation.__init__(self, space, msg_space) 14 | self.attachOrigin = attachOrigin 15 | self.newDim = None 16 | self.newDimMenu = NewDimMenu(space, attachOrigin, 17 | self.toDimInsertStage) 18 | 19 | def cancel(self): 20 | Operation.cancel(self) 21 | self.newDimMenu.close() 22 | 23 | def __del__(self): 24 | self.newDimMenu.submit.disconnect(self.toDimInsertStage) 25 | 26 | def show(self): 27 | self.newDimMenu.open() 28 | self.report("Below is a space to enter the name of a new dimension. Enter a name and press ENTER to create that dimension. Or press ESC to cancel.") 29 | 30 | def toDimInsertStage(self, dim): 31 | self.newDimMenu.close() 32 | if self.space.nameDim(dim): 33 | self.finish("New dim: '{0}' created successfully.".format(dim)) 34 | else: 35 | self.finish("Dim '{0}' already exists.".format(dim)) 36 | 37 | def processKeys(self, k, mods): 38 | if self.needsInput: 39 | if k == Qt.Key_Escape: 40 | if self.newDimMenu.isOpen(): 41 | self.newDimMenu.close() 42 | self.finish("New dimension creation cancelled.") 43 | return True 44 | return False 45 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Welcome to 2 | ____ _ 3 | | _ \(_)_ __ ___ ___ ___ __ _ _ __ ___ 4 | | | | | | '_ ` _ \/ __|/ __/ _` | '_ \ / _ \ 5 | | |_| | | | | | | \__ \ (_| (_| | |_) | __/ 6 | |____/|_|_| |_| |_|___/\___\__,_| .__/ \___| 7 | |_| 8 | 9 | Dimscape is a navigator and editor for a multi-dimensional database 10 | structure termed a ZZStructure by Project Xanadu. It is written in Python 11 | and makes heavy use of the python Qt bindings. 12 | 13 | Building: 14 | 15 | First, install necessary libraries: 16 | sudo apt-get install python-qt4-dev python-qt4-phonon 17 | 18 | To build Dimscape, run: 19 | python setup.py build 20 | 21 | you can then either run Dimscape from the folder as: 22 | python ./bin/dimscape 23 | 24 | or install it with: 25 | sudo python setup.py install 26 | 27 | Navigation: 28 | 29 | Arrow keys: basic up-down-left-right navigation from cell to cell 30 | Ctrl-Up/Ctrl-Down: move posward (toward you)/negward (toward screen) 31 | in the Z dimension 32 | 33 | c: create a new cell as prompted 34 | l/L: create/remove a link between two cells as prompted 35 | r: remove a neighboring cell as prompted 36 | d: create a new dimension 37 | x/X: cycle forward/backward through the dimensions bound to the X 38 | direction 39 | y/Y: cycle forward/backward through the dimensions bound to the Y 40 | direction 41 | z/Z: cycle forward/backward through the dimensions bound to the Z 42 | direction 43 | s: swap the dimensions bound to the X and Y directions 44 | i: edit a text cell, press ESC to return to normal navigation 45 | Enter: execute the contents of a cell, for video cells this toggles 46 | play/pause 47 | -------------------------------------------------------------------------------- /dimscape/types/system.py: -------------------------------------------------------------------------------- 1 | from __future__ import generators, print_function 2 | # -*- coding: utf-8 -*- 3 | 4 | from PyQt4.QtCore import pyqtSlot 5 | from PyQt4.QtGui import QColor 6 | 7 | from text import TextCell 8 | 9 | class SystemWarnCell(TextCell): 10 | 11 | def __init__(self, typeName, cid, data, cons=None, props=None): 12 | msg = "\'" + typeName + "\'" + " has not be registered with the type system." 13 | TextCell.__init__(self, cid, msg, cons, props, editable=False) 14 | self.typeName = typeName 15 | self._data = data 16 | 17 | def placeChildren(self, space): 18 | TextCell.placeChildren(self, space) 19 | self.getChild().setBrush(QColor("red")) 20 | 21 | @property 22 | def data(self): 23 | return self._data 24 | 25 | 26 | class ProgCell(TextCell): 27 | 28 | typeInfo = "An in to executable code." 29 | 30 | def __init__(self, cellId, data=None, cons=None, props=None): 31 | msg = (data and data[0]) or "" 32 | TextCell.__init__(self, cellId, msg, cons, props, editable=False) 33 | self._data = data 34 | self.execute = None 35 | 36 | def placeChildren(self, space): 37 | TextCell.placeChildren(self, space) 38 | msg, fun, args = self.data 39 | if callable(fun): 40 | self.execute = lambda: fun(*args) 41 | else: 42 | self.execute = lambda: 0 43 | 44 | @property 45 | def data(self): 46 | return self._data 47 | @data.setter 48 | def data(self, val): 49 | if len(val) != 3 or not callable(val[1]): 50 | raise ValueError("Cannot create prog cell with invalid parameter: {0}".format(val)) 51 | super(TextCell, self).data = val[0] 52 | self._data = val 53 | 54 | def createData(self): 55 | # our default is all we need 56 | pass 57 | 58 | @pyqtSlot() 59 | def edit(self): 60 | text = self.getChild() 61 | text.setFocus() 62 | 63 | -------------------------------------------------------------------------------- /dimscape/menus/newDim.py: -------------------------------------------------------------------------------- 1 | from __future__ import generators, print_function 2 | # -*- coding: utf-8 -*- 3 | 4 | from dimscape.types import CellTypeRegistrar 5 | 6 | class NewDimMenu(object): 7 | 8 | def __init__(self, space, attachCell, submitCallback): 9 | object.__init__(self) 10 | self.space = space 11 | self.attachCell = attachCell 12 | self.amOpen = False 13 | self.entryCell = None 14 | self.submitCell = None 15 | self.submitCallback = submitCallback 16 | 17 | def isOpen(self): 18 | return self.amOpen 19 | 20 | def createDim(self): 21 | dim = str(self.entryCell.getText()) 22 | self.close() 23 | self.submitCallback(dim) 24 | 25 | def open(self): 26 | reg = CellTypeRegistrar.get() 27 | self.amOpen = True 28 | self.space.pushDims() 29 | self.space.setDim(self.space.X, ".ds.submit") 30 | self.space.setDim(self.space.Y, ".ds.entry") 31 | self.space.setDim(self.space.Z, ".ds.nil") 32 | 33 | self.entryCell = self.space.makeTransientCell(reg.fromName("text"), 34 | "") 35 | self.submitCell = self.space.makeTransientCell(reg.fromName("prog"), 36 | ("Submit", self.createDim, ())) 37 | # link up entry cell 38 | self.space.link(self.attachCell, self.space.POS, 39 | self.space.Y, self.entryCell) 40 | # link up submit button 41 | self.space.link(self.entryCell, self.space.POS, 42 | self.space.X, self.submitCell) 43 | # We want return to submit, just like a normal text entry 44 | self.entryCell.execute = self.createDim 45 | self.space.redraw() 46 | 47 | def close(self): 48 | self.amOpen = False 49 | self.space.setAcursed(self.attachCell) 50 | self.entryCell.unlink(repair=False) 51 | self.cleanup() 52 | self.space.popDims() 53 | self.space.redraw() 54 | 55 | def cleanup(self): 56 | # Since an unlink isn't nescessary and these are transient cells 57 | # we don't need to go through self.space.removeCell 58 | for cell in [self.entryCell, self.submitCell]: 59 | cell.remove(self.space.scene, cached=False) 60 | -------------------------------------------------------------------------------- /dimscape/types/text.py: -------------------------------------------------------------------------------- 1 | from __future__ import generators, print_function 2 | # -*- coding: utf-8 -*- 3 | 4 | from PyQt4 import QtGui, QtCore 5 | from PyQt4.QtCore import pyqtSlot, QPointF 6 | 7 | from cell import CellSkin 8 | 9 | import logging 10 | dlog = logging.getLogger("dimscape.types") 11 | 12 | class TextCell(CellSkin): 13 | 14 | typeInfo = "Some text to commence with the reading. May be file backed, may be not." 15 | 16 | def __init__(self, cellId, text=None, cons=None, props=None, editable=True): 17 | CellSkin.__init__(self, cellId, text or "", cons, props) 18 | self.initData = self._data 19 | self.editable = editable 20 | 21 | def execute(self): 22 | pass 23 | 24 | def placeChildren(self, space): 25 | if self.editable: Text = QtGui.QGraphicsTextItem 26 | else: Text = QtGui.QGraphicsSimpleTextItem 27 | if self.dataInline: 28 | text = Text(self.initData, self.getSkin()) 29 | elif os.path.exists(self.data): 30 | filey = file(self.initData, "r") 31 | conts = filey.read() 32 | filey.close() 33 | text = Text(conts, self.getSkin()) 34 | self.space = space 35 | self.initData = None 36 | if self.editable: 37 | qt = QtCore.Qt 38 | text.setTextInteractionFlags(qt.TextEditorInteraction|qt.TextBrowserInteraction) 39 | text.document().contentsChanged.connect(self.redrawMe) 40 | 41 | @pyqtSlot() 42 | def redrawMe(self): 43 | if not self.initialMove: 44 | self.space.chugDraw() 45 | 46 | def createData(self, scene): 47 | # our default is all we need 48 | pass 49 | 50 | def getText(self): 51 | if self.editable: 52 | return self.getChild().toPlainText() 53 | return self.getChild().text() 54 | 55 | def setText(self, text): 56 | wid = self.getChild() 57 | if self.editable: wid.setPlainText(text) 58 | else: wid.setText(text) 59 | 60 | def remove(self, scene, cached=True): 61 | if self.loaded and not cached: 62 | self.initData = self.data 63 | CellSkin.remove(self, scene, cached) 64 | 65 | @property 66 | def data(self): 67 | if self.skin: 68 | return unicode(self.getText()) 69 | return self.initData 70 | @data.setter 71 | def data(self, val): 72 | if self.skin: 73 | self.setText(val) 74 | else: 75 | self.initData = val 76 | 77 | @pyqtSlot() 78 | def edit(self): 79 | if self.editable: 80 | text = self.getChild() 81 | text.setFocus() 82 | 83 | -------------------------------------------------------------------------------- /dimscape/types/registrar.py: -------------------------------------------------------------------------------- 1 | from __future__ import generators, print_function 2 | # -*- coding: utf-8 -*- 3 | 4 | from PyQt4.QtCore import QObject, pyqtSignal 5 | from cell import Cell 6 | from system import SystemWarnCell, ProgCell 7 | 8 | class CellTypeRegistrar(QObject): 9 | 10 | dynamicCellsRegistered = pyqtSignal(list) 11 | 12 | registrar = None 13 | 14 | def __init__(self): 15 | QObject.__init__(self) 16 | self.typeToName = {} 17 | self.nameToType = {} 18 | self.nameToProps = {} 19 | self.cellsToRegister = {} 20 | 21 | def fromType(self, cell_or_type): 22 | if hasattr(cell_or_type, "typeName"): 23 | return cell_or_type.typeName 24 | if isinstance(cell_or_type, Cell): 25 | return self.typeToName[type(cell_or_type)] 26 | return self.typeToName[cell_or_type] 27 | 28 | def fromName(self, aName): 29 | return self.nameToType.get(aName, None) 30 | 31 | @classmethod 32 | def get(cls): 33 | if not cls.registrar: 34 | cls.registrar = CellTypeRegistrar() 35 | return cls.registrar 36 | 37 | def types(self): 38 | return self.typeToName.iterkeys() 39 | 40 | def names(self): 41 | return self.nameToType.iterkeys() 42 | 43 | def registrants(self): 44 | return self.nameToType.iteritems() 45 | 46 | def isRegistered(self, aIdent): 47 | if isinstance(aIdent, str): 48 | return (aIdent in self.nameToType) 49 | return (aIdent in self.typeToName) 50 | 51 | def typeInfo(self, aType): 52 | if hasattr(aType, "typeInfo"): 53 | return aType.typeInfo 54 | return aType.__doc__ 55 | 56 | def register(self, aName, aType, props=None, system=False): 57 | # TODO: make system and user types, user types are 58 | # enumerable, createable by the user, system types 59 | # like SystemWarnCell are only used internally or by 60 | # kaukatcr/python extensions 61 | if not system: 62 | self.typeToName[aType] = aName 63 | self.nameToType[aName] = aType 64 | if props: 65 | self.nameToProps[aName] = props 66 | if aName in self.cellsToRegister: 67 | cells = self.cellsToRegister[aName] 68 | self.typeCastCells(cells, aType) 69 | self.cellsToRegister.pop(aName) 70 | 71 | def typeCast(self, cells, newType): 72 | for i in xrange(len(cells)): 73 | cells[i] = newType(cell.cellId, cell.data, cell.cons) 74 | self.dynamicCellsRegistered(c) 75 | 76 | def registerMany(self, iterable): 77 | for (n, t) in iterable: 78 | self.register(n, t) 79 | 80 | def registerDynamicCell(self, typeName, cid, data, cons=None): 81 | dynCells = self.cellsToRegister.setdefault(typeName, []) 82 | t = SystemWarnCell(typeName, cid, data, cons) 83 | dynCells.append(t) 84 | return t 85 | 86 | -------------------------------------------------------------------------------- /dimscape/ops/newCell.py: -------------------------------------------------------------------------------- 1 | from __future__ import generators, print_function 2 | # -*- coding: utf-8 -*- 3 | 4 | from PyQt4.QtCore import Qt 5 | 6 | from operation import Operation 7 | from dimscape.menus import NewCellMenu 8 | from dimscape.types.cell import Cell 9 | 10 | class NewCellOperation(Operation): 11 | 12 | def __init__(self, space, attachOrigin, msg_space): 13 | Operation.__init__(self, space, msg_space) 14 | self.attachOrigin = attachOrigin 15 | self.newCell = None 16 | self.newCellMenu = NewCellMenu(space, attachOrigin, 17 | self.toCellInsertStage) 18 | 19 | def cancel(self): 20 | Operation.cancel(self) 21 | self.newCellMenu.close() 22 | 23 | def __del__(self): 24 | self.newCellMenu.submit.disconnect(self.toCellInsertStage) 25 | 26 | def show(self): 27 | self.newCellMenu.open() 28 | self.report("Below is a list of cell types. Move the cursor over one and press ENTER to select the type of your new cell. Or press ESC to cancel.") 29 | 30 | def toCellInsertStage(self, cell): 31 | self.report("Press a navigation key to attach the new cell along that apparent dimension.") 32 | self.newCell = cell 33 | 34 | def processKeys(self, k, mods): 35 | if self.needsInput: 36 | if k == Qt.Key_Escape: 37 | if self.newCellMenu.isOpen(): 38 | self.newCellMenu.close() 39 | if self.newCell: 40 | self.space.removeCell(self.newCell) 41 | self.newCell = None 42 | self.space.redraw() 43 | self.finish("Connection attempt cancelled.") 44 | if not self.newCell: 45 | return False 46 | return self.processCellInsertKeys(k, mods) 47 | return True 48 | 49 | def processCellInsertKeys(self, k, mods): 50 | linkDir = None 51 | if k == Qt.Key_Up: 52 | if mods == Qt.ControlModifier: 53 | linkDir = (self.space.NEG, self.space.Z) 54 | else: 55 | linkDir = (self.space.NEG, self.space.Y) 56 | elif k == Qt.Key_Down: 57 | if mods == Qt.ControlModifier: 58 | linkDir = (self.space.POS, self.space.Z) 59 | else: 60 | linkDir = (self.space.POS, self.space.Y) 61 | elif k == Qt.Key_Right: 62 | linkDir = (self.space.POS, self.space.X) 63 | elif k == Qt.Key_Left: 64 | linkDir = (self.space.NEG, self.space.X) 65 | 66 | if linkDir: 67 | direc, appDim = linkDir 68 | self.space.makeCellConcrete(self.newCell) 69 | self.space.link(self.attachOrigin, direc, appDim, 70 | self.newCell) 71 | self.newCell.add(self.space) 72 | self.space.setAcursed(self.newCell) 73 | self.finish("New cell created successfully.") 74 | # we are responsible for the changes we make to scene 75 | self.space.redraw() 76 | # We consumed the key, so inform the client not to try it 77 | return True 78 | return False 79 | -------------------------------------------------------------------------------- /dimscape/ops/unlinkCell.py: -------------------------------------------------------------------------------- 1 | from __future__ import generators, print_function 2 | # -*- coding: utf-8 -*- 3 | 4 | from PyQt4.QtCore import Qt 5 | 6 | from operation import Operation 7 | 8 | class UnlinkCellOperation(Operation): 9 | 10 | def __init__(self, space, attachCell, msg_space, allLinks=True): 11 | Operation.__init__(self, space, msg_space) 12 | self.attachCell = attachCell 13 | self.allLinks = allLinks 14 | 15 | def show(self): 16 | if self.allLinks: 17 | self.report("Press a navigation key to disconnect that cell from all cells in all dimensions and remove it, or ESC to cancel.") 18 | else: 19 | self.report("Press a navigation key to disconnect that cell from the currently selected cell, or ESC to cancel.") 20 | 21 | def removeCell(self, connected_cell, aDir, appDim): 22 | if self.allLinks: 23 | self.space.removeCell(connected_cell) 24 | else: 25 | self.space.removeLink(self.attachCell, appDim, direction=aDir) 26 | self.space.redraw() 27 | 28 | def processKeys(self, k, mods): 29 | if self.needsInput: 30 | return self.processPickDirKeys(k, mods) 31 | return False 32 | 33 | def processPickDirKeys(self, k, mods): 34 | linkDir = None 35 | if k == Qt.Key_Escape: 36 | if self.allLinks: self.finish("Cell deletion attempt cancelled.") 37 | else: self.finish("Link deletion attempt cancelled.") 38 | return True 39 | elif k == Qt.Key_Up: 40 | if mods == Qt.ControlModifier: 41 | linkDir = (self.space.NEG, self.space.Z) 42 | else: 43 | linkDir = (self.space.NEG, self.space.Y) 44 | elif k == Qt.Key_Down: 45 | if mods == Qt.ControlModifier: 46 | linkDir = (self.space.POS, self.space.Z) 47 | else: 48 | linkDir = (self.space.POS, self.space.Y) 49 | elif k == Qt.Key_Right: 50 | linkDir = (self.space.POS, self.space.X) 51 | elif k == Qt.Key_Left: 52 | linkDir = (self.space.NEG, self.space.X) 53 | 54 | if linkDir: 55 | direc, appDim = linkDir 56 | if self.attachCell.hasCon(self.space.getDim(appDim), direc): 57 | cony = self.attachCell.getCon( 58 | self.space.getDim(appDim), direc) 59 | cid = cony.cellId 60 | self.removeCell(cony, direc, appDim) 61 | if self.allLinks: 62 | self.finish("Disconnected and removed cell {0} \ 63 | successfully.".format(cid)) 64 | else: 65 | self.finish("Disconnected cell {0} successfully.".format(cid)) 66 | else: 67 | self.reportLinkUnOccupiedError(linkDir) 68 | # We consumed the key, so inform the client not to try it 69 | return True 70 | return False 71 | 72 | def reportLinkUnOccupiedError(self, linkDir): 73 | strDir = self.space.dirToString(linkDir[0]) 74 | strDim = self.space.dimToString(linkDir[1]) 75 | tmpl8 = "Error: The link (%s, %s) is unoccupied. " + \ 76 | "You can either pick an occupied space or cancel with ESC." 77 | self.reportError(tmpl8 % (strDir, strDim)) 78 | -------------------------------------------------------------------------------- /dimscape/menus/newCell.py: -------------------------------------------------------------------------------- 1 | from __future__ import generators, print_function 2 | # -*- coding: utf-8 -*- 3 | 4 | from dimscape.types import CellTypeRegistrar 5 | from dimscape.types.cell import Cell 6 | 7 | class NewCellMenu(object): 8 | 9 | def __init__(self, space, attachCell, submitCallback): 10 | object.__init__(self) 11 | self.space = space 12 | self.attachCell = attachCell 13 | self.amOpen = False 14 | self.transientCells = [] 15 | self.submitCallback = submitCallback 16 | 17 | def isOpen(self): 18 | return self.amOpen 19 | 20 | def _createTypedCell(self, aType): 21 | self.close() # acursed cell back at start 22 | # TODO: bad things will happen if anyone else fiddles with 23 | # self.cells in another thread, when kaukatcr comes in, many 24 | # changes will need to be made 25 | cell = self.space.makeTransientCell(aType) 26 | cell.createData(self.space.scene) 27 | self.submitCallback(cell) 28 | 29 | def open(self): 30 | reg = CellTypeRegistrar.get() 31 | self.amOpen = True 32 | self.space.pushDims() 33 | self.space.setDim(self.space.X, ".ds.new-cell-type-info") 34 | self.space.setDim(self.space.Y, ".ds.new-cell-types") 35 | self.space.setDim(self.space.Z, ".ds.new-cell-subtypes") 36 | chugCell = self.space.acursedCell 37 | infoChugCell = None 38 | 39 | prog = reg.fromName("prog") 40 | text = reg.fromName("text") 41 | 42 | for (n, t) in reg.registrants(): 43 | # the tuple operator is ',' never forget 44 | cell = self.space.makeTransientCell(prog, 45 | (n, self._createTypedCell, (t,))) 46 | infoCell = self.space.makeTransientCell(text, 47 | reg.typeInfo(t)) 48 | self.transientCells.extend([cell, infoCell]) 49 | # hook up our prog cell downward 50 | self.space.link(chugCell, self.space.POS, self.space.Y, 51 | cell) 52 | # hook up info cell rightward 53 | self.space.link(cell, self.space.POS, self.space.X, 54 | infoCell) 55 | # hook up info cell downward 56 | if infoChugCell: 57 | self.space.link(infoChugCell, self.space.POS, 58 | self.space.Y, infoCell) 59 | chugCell = cell 60 | infoChugCell = infoCell 61 | # hook up our prog cell ring rank 62 | self.space.link(self.transientCells[-2], self.space.POS, 63 | self.space.Y, self.attachCell) 64 | # hook up info cell ring rank 65 | self.space.link(self.transientCells[-1], self.space.POS, 66 | self.space.Y, self.transientCells[1]) 67 | self.space.redraw() 68 | 69 | def close(self): 70 | self.amOpen = False 71 | self.space.setAcursed(self.attachCell) 72 | # Only the head cell/tail cell is linked to a concrete cell 73 | self.transientCells[0].unlink(repair=False) 74 | self.transientCells[-2].unlink(repair=False) 75 | self.cleanup() 76 | self.space.popDims() 77 | self.space.redraw() 78 | 79 | def cleanup(self): 80 | for cell in self.transientCells: 81 | cell.remove(self.space.scene, cached=False) 82 | self.transientCells = [] 83 | 84 | -------------------------------------------------------------------------------- /dimscape/ops/linkCell.py: -------------------------------------------------------------------------------- 1 | from __future__ import generators, print_function 2 | # -*- coding: utf-8 -*- 3 | 4 | from PyQt4.QtCore import Qt 5 | 6 | from operation import Operation 7 | 8 | class LinkOperation(Operation): 9 | 10 | def __init__(self, space, linkOrigin, msg_space): 11 | Operation.__init__(self, space, msg_space) 12 | self.linkOrigin = linkOrigin 13 | self.linkDir = None 14 | self.onPickDirStage = True 15 | 16 | def show(self): 17 | self.report("Press a navigation key to connect a cell " + \ 18 | "to that open location.") 19 | 20 | def processKeys(self, k, mods): 21 | if self.needsInput: 22 | if self.onPickDirStage: 23 | return self.processPickDirKeys(k, mods) 24 | return self.processMarkTargetKeys(k, mods) 25 | return False 26 | 27 | def processMarkTargetKeys(self, k, mods): 28 | 29 | if k == Qt.Key_M: 30 | direc, appDim = self.linkDir[0], self.linkDir[1] 31 | markCell = self.space.acursedCell 32 | if markCell.hasCon(self.space.getDim(appDim)): 33 | if not self.linkOrigin.hasCon(self.space.getDim(appDim), 34 | direc): 35 | markDir = markCell.invertDir(direc) 36 | self.space.removeLink(markCell, appDim, markDir) 37 | else: 38 | self.space.removeLink(markCell, appDim) 39 | self.space.link(self.linkOrigin, direc, appDim, 40 | markCell) 41 | self.space.redraw() 42 | self.reportSuccessfulCompletion() 43 | return True 44 | return False 45 | 46 | def processPickDirKeys(self, k, mods): 47 | 48 | if k == Qt.Key_Escape: 49 | self.finish("Connection attempt cancelled.") 50 | elif k == Qt.Key_Up: 51 | if mods == Qt.ControlModifier: 52 | self.linkDir = (self.space.NEG, self.space.Z) 53 | else: 54 | self.linkDir = (self.space.NEG, self.space.Y) 55 | elif k == Qt.Key_Down: 56 | if mods == Qt.ControlModifier: 57 | self.linkDir = (self.space.POS, self.space.Z) 58 | else: 59 | self.linkDir = (self.space.POS, self.space.Y) 60 | elif k == Qt.Key_Right: 61 | self.linkDir = (self.space.POS, self.space.X) 62 | elif k == Qt.Key_Left: 63 | self.linkDir = (self.space.NEG, self.space.X) 64 | 65 | if self.linkDir: 66 | direc, appDim = self.linkDir 67 | self.onPickDirStage = False 68 | self.report("Selection Stage complete! Press M to mark " + \ 69 | "a cell for linking.") 70 | # We consumed the key, so inform the client not to try it 71 | return True 72 | return False 73 | 74 | def reportSuccessfulCompletion(self): 75 | strDir = self.space.dirToString(self.linkDir[0]).lower() 76 | strDim = self.space.dimToString(self.linkDir[1]) 77 | tmpl8 = "Connected (%d) to (%d) %s on the %s dimension successfully." 78 | self.finish(tmpl8 % (self.linkOrigin.cellId, 79 | self.space.acursedCell.cellId, strDir, strDim)) 80 | 81 | def reportLinkOccupiedError(self): 82 | strDir = self.space.dirToString(self.linkDir[0]) 83 | strDim = self.space.dimToString(self.linkDir[1]) 84 | tmpl8 = "Error: The link (%s, %s) is occupied. " + \ 85 | "You can either pick an open space or cancel with ESC." 86 | self.reportError(tmpl8 % (strDir, strDim)) 87 | -------------------------------------------------------------------------------- /dimscape/types/media.py: -------------------------------------------------------------------------------- 1 | from __future__ import generators, print_function 2 | # -*- coding: utf-8 -*- 3 | 4 | from PyQt4 import QtGui, QtCore 5 | from PyQt4.QtCore import pyqtSlot 6 | from PyQt4.QtGui import QColor 7 | try: 8 | # I hear KDE's version of Phonon is more recent 9 | # use it if available 10 | from PyKDE4.phonon import Phonon 11 | except ImportError: 12 | from PyQt4.phonon import Phonon 13 | 14 | import os 15 | 16 | from cell import CellSkin 17 | 18 | class VideoCell(CellSkin): 19 | 20 | typeInfo = "A video for to be playing which is backed by a file on your filesystem." 21 | 22 | initialSeekTime = 1000 23 | 24 | def __init__(self, cellId, path=None, cons=None, props=None): 25 | CellSkin.__init__(self, cellId, path or "", cons) 26 | self.canSeek = False 27 | self.imageReplaced = False 28 | self.fileNotFound = False 29 | self.sought = False 30 | self.posterImage = None 31 | 32 | @QtCore.pyqtSlot(int) 33 | def buffer_change(self, percent): 34 | print ("Buffer percent:", percent) 35 | 36 | @QtCore.pyqtSlot(int, int) 37 | def state_change(self, newState, oldState): 38 | print (oldState, "->", newState) 39 | if self.canSeek and not self.sought and newState == Phonon.PausedState: 40 | vid = self.getChild().widget() 41 | # Do a one off seek to get us past the black 42 | self.sought = True 43 | # We need to get the ticks going 44 | vid.play() 45 | vid.seek(self.initialSeekTime) 46 | print ("time after seek:", vid.currentTime()) 47 | 48 | @QtCore.pyqtSlot(bool) 49 | def seek_change(self, canSeek): 50 | print ("Can seek?", canSeek) 51 | self.canSeek = canSeek 52 | 53 | @pyqtSlot() 54 | def finish(self): 55 | print ("called finished") 56 | self.replaceWithPosterImage() 57 | 58 | @pyqtSlot(int) 59 | def ticked(self): 60 | wid = self.getChild().widget() 61 | if wid.currentTime() >= 0: 62 | # Our seek finally came through 63 | # Cancel tick 64 | wid.mediaObject().tick.disconnect(self.ticked) 65 | wid.mediaObject().setTickInterval(0) 66 | wid.pause() 67 | self.replaceWithPosterImage() 68 | 69 | def replaceWithPosterImage(self): 70 | print ("replacing with poster image") 71 | gwid = self.getChild() 72 | scene = gwid.scene() 73 | scene.removeItem(gwid) 74 | if not self.posterImage: 75 | pix = QtGui.QPixmap.grabWidget(gwid.widget()) 76 | self.posterImage = QtGui.QGraphicsPixmapItem(pix, self.getSkin()) 77 | else: 78 | self.posterImage.setParentItem(self.getSkin()) 79 | self.imageReplaced = True 80 | 81 | def removePosterImage(self): 82 | print ("removing poster image") 83 | gpix = self.getChild() 84 | scene = gpix.scene() 85 | scene.removeItem(gpix) 86 | self.loadVideo() 87 | self.imageReplaced = False 88 | 89 | def placeChildren(self, space): 90 | # TODO: This ignores dataInline atm 91 | # videos should be out-of-line by default 92 | if os.path.exists(self.data): 93 | vid = self.loadVideo() 94 | mobj = vid.mediaObject() 95 | mobj.setTickInterval(100) 96 | mobj.tick.connect(self.ticked) 97 | else: 98 | text = QtGui.QGraphicsSimpleTextItem(self.getSkin()) 99 | text.setText("\'" + self.data + "\'" + " could not be found.") 100 | text.setBrush(QColor("red")) 101 | self.fileNotFound = True 102 | 103 | def loadVideo(self): 104 | vid = Phonon.VideoPlayer(Phonon.VideoCategory) 105 | mobj = vid.mediaObject() 106 | mobj.stateChanged.connect(self.state_change) 107 | mobj.bufferStatus.connect(self.buffer_change) 108 | mobj.seekableChanged.connect(self.seek_change) 109 | vid.load(Phonon.MediaSource(self.data)) 110 | vid.resize(QtCore.QSize(320, 240)) 111 | vid.finished.connect(self.finish) 112 | proxy_wid = QtGui.QGraphicsProxyWidget(self.getSkin()) 113 | proxy_wid.setWidget(vid) 114 | vid.pause() 115 | return vid 116 | 117 | @pyqtSlot() 118 | def execute(self): 119 | if not self.fileNotFound: 120 | if self.imageReplaced: 121 | self.removePosterImage() 122 | vid = self.getChild().widget() 123 | if vid.isPlaying(): 124 | vid.pause() 125 | else: 126 | vid.play() 127 | 128 | @pyqtSlot() 129 | def edit(self): 130 | pass 131 | 132 | def createData(self, scene): 133 | main = scene.views()[0] 134 | self.data = QtGui.QFileDialog.getOpenFileName(main, 135 | QtCore.QObject().tr("Open Video File"), 136 | QtCore.QDir.homePath()) 137 | if self.data: 138 | self.data = str(self.data) 139 | 140 | 141 | class AudioCell(CellSkin): 142 | pass 143 | 144 | class ImageCell(CellSkin): 145 | pass 146 | -------------------------------------------------------------------------------- /dimscape/jsonBackend.py: -------------------------------------------------------------------------------- 1 | from __future__ import generators, print_function 2 | # -*- coding: utf-8 -*- 3 | """ 4 | This is the "backend" module. 5 | 6 | It provides the DJSONBackend class, which encapsulates our physical data. 7 | You start her up, and then query. Each change of value *should* be stored. 8 | 9 | """ 10 | import sys, os 11 | try: 12 | import simplejson as json 13 | except ImportError: 14 | try: 15 | import json 16 | except ImportError: 17 | print ("Either json or simplejson is required to run Dimscape", file=sys.stderr) 18 | from dimscape.types import CellTypeRegistrar 19 | from dimscape.types.cell import Cell 20 | 21 | class DJSONBackend(object): 22 | 23 | NEG = Cell.NEG 24 | POS = Cell.POS 25 | 26 | reg = CellTypeRegistrar.get() 27 | 28 | def __setattr__(self, name, val): 29 | self.__dict__[name] = val 30 | 31 | def __init__(self, filey=None): 32 | # Only want this ever done once, since we will have a backend 33 | # managing each view 34 | object.__init__(self) 35 | self.filey = filey 36 | if self.filey: 37 | self.load(filey) 38 | 39 | @classmethod 40 | def makeCell(cls, cid, json_cells): 41 | # deleted cells stored as python None, json 'null' 42 | cinfo = json_cells[cid] 43 | if not cinfo: 44 | return None 45 | if not "type" in cinfo: 46 | raise ValueError("A JSON cell must contain a 'type' field") 47 | tName = cinfo["type"] 48 | cData = cinfo["data"] 49 | constructor = cls.reg.fromName(tName) 50 | if constructor: 51 | # save cons for cellizeCons later 52 | madeCell = constructor(cid, cData) 53 | else: 54 | madeCell = cls.reg.registerDynamicCell(tName, cid, cData) 55 | return madeCell 56 | 57 | @classmethod 58 | def fromCell(cls, cell): 59 | cType = cls.reg.fromType(cell) 60 | return { "type": cType, "data": cell.data, 61 | "cons": cls.freezeCellCons(cell) } 62 | 63 | @staticmethod 64 | def createNew(rootCell): 65 | me = DJSONBackend() 66 | DJSONBackend.createDummy(me) 67 | me.cells.append(rootCell) 68 | me.acursedId = 0 69 | me.acursedIds.append(me.acursedId) 70 | return me 71 | 72 | @staticmethod 73 | def createDummy(back): 74 | # Eventually version will be useful for something 75 | back.version = 1 76 | for i in ["allDims", "acursedIds", "dimConfig", "cells"]: 77 | back.__setattr__(i, []) 78 | startDims = [".ds.1", ".ds.2", ".ds.3"] 79 | back.allDims.extend(startDims) 80 | back.dimConfig.extend(list(startDims)) 81 | 82 | def load(self, filey): 83 | # I want to use 'with filey:' here, but for testing purposes 84 | # filey might be a StringIO, which doesn't humor me with an 85 | # __exit__/__enter__ method pair 86 | self.filey = filey 87 | space = json.load(self.filey) 88 | self.loadMetadata(space) 89 | self.loadCells(space) 90 | filey.close() 91 | 92 | @staticmethod 93 | def freezeCellCons(cell): 94 | cons = {} 95 | for (dim, d) in cell.cons.iteritems(): 96 | if None != d[0] and None != d[1]: 97 | cons.update({dim: [d[0].cellId, d[1].cellId]}) 98 | elif None != d[0]: 99 | cons.update({dim: [d[0].cellId, -1]}) 100 | else: 101 | cons.update({dim: [-1, d[1].cellId]}) 102 | return cons 103 | 104 | def thawCellCons(self, cell, json_cell): 105 | for (dim, direcs) in json_cell["cons"].iteritems(): 106 | if -1 != direcs[0]: 107 | targetCell = self.cells[direcs[0]] 108 | cell.addNegCon(dim, targetCell) 109 | if -1 != direcs[1]: 110 | targetCell = self.cells[direcs[1]] 111 | cell.addPosCon(dim, targetCell) 112 | 113 | def loadCells(self, space): 114 | json_cells = space["cells"] 115 | self.cells = [] 116 | # we are O(n^2) atm 117 | for i in xrange(len(json_cells)): 118 | self.cells.append(self.makeCell(i, json_cells)) 119 | for i in xrange(len(json_cells)): 120 | self.thawCellCons(self.cells[i], json_cells[i]) 121 | 122 | def loadMetadata(self, space): 123 | # Eventually version will be useful for something 124 | self.version = space["version"] 125 | for i in ["allDims", "acursedIds", "dimConfig"]: 126 | self.__setattr__(i, space[i]) 127 | self.acursedId = self.acursedIds[0] 128 | 129 | def saveMetadata(self, space): 130 | self.acursedIds[0] = self.acursedId 131 | space["version"] = self.version 132 | for i in ["allDims", "acursedIds", "dimConfig"]: 133 | space[i] = self.__getattribute__(i) 134 | 135 | def saveCells(self, space): 136 | space["cells"] = [] 137 | # Heard about this trick from python tricks somewhere 138 | cellsApp = space["cells"].append 139 | for c in self.cells: 140 | cellsApp(self.fromCell(c)) 141 | 142 | def saveAs(self, filey): 143 | """Save everything to filey's location and direct further operations 144 | there, thereafter.""" 145 | self.filey = filey 146 | self.save() 147 | 148 | def save(self): 149 | if self.filey: 150 | space = {} 151 | self.saveMetadata(space) 152 | self.saveCells(space) 153 | if hasattr(self.filey, 'name'): 154 | # We are probably a file-backed thing 155 | self.filey = file(self.filey.name, "w") 156 | json.dump(space, self.filey) 157 | self.filey.close() # close should flush 158 | else: 159 | # StringIO, something file-like, probably 160 | # for testing 161 | self.filey.truncate(0) 162 | json.dump(self.space, self.filey) 163 | # 164 | -------------------------------------------------------------------------------- /dimscape/main_window.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 800 10 | 600 11 | 12 | 13 | 14 | 15 | 0 16 | 0 17 | 18 | 19 | 20 | Dimscape 21 | 22 | 23 | 24 | 25 | 0 26 | 0 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | Qt::Horizontal 37 | 38 | 39 | 40 | 40 41 | 20 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 0 52 | 0 53 | 800 54 | 24 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | &File 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | &Dimension 76 | 77 | 78 | 79 | 80 | 81 | &Cell 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | toolBar 95 | 96 | 97 | TopToolBarArea 98 | 99 | 100 | false 101 | 102 | 103 | 104 | 105 | &New Space 106 | 107 | 108 | Ctrl+N 109 | 110 | 111 | 112 | 113 | &Open Space 114 | 115 | 116 | Ctrl+L 117 | 118 | 119 | 120 | 121 | &Quit 122 | 123 | 124 | Ctrl+Q 125 | 126 | 127 | 128 | 129 | X 130 | 131 | 132 | 133 | 134 | Y 135 | 136 | 137 | 138 | 139 | Z 140 | 141 | 142 | 143 | 144 | Edit Cell 145 | 146 | 147 | 148 | 149 | Insert Cell 150 | 151 | 152 | 153 | 154 | Delete Cell 155 | 156 | 157 | 158 | 159 | &Save Space 160 | 161 | 162 | Ctrl+S 163 | 164 | 165 | 166 | 167 | Save Space &As 168 | 169 | 170 | 171 | 172 | New Dimension 173 | 174 | 175 | Ctrl+D 176 | 177 | 178 | 179 | 180 | Close 181 | 182 | 183 | 184 | 185 | 186 | 187 | -------------------------------------------------------------------------------- /dimscape/connection.py: -------------------------------------------------------------------------------- 1 | from __future__ import generators, print_function 2 | # -*- coding: utf-8 -*- 3 | 4 | from PyQt4 import QtCore, QtGui 5 | from PyQt4.QtCore import pyqtSlot, QSizeF, QRectF, QPointF 6 | from PyQt4.QtGui import QPen, QBrush, QColor 7 | 8 | from dimscape.types.cell import Cell 9 | 10 | import logging 11 | dlog = logging.getLogger("dimscape.cons") 12 | 13 | class Connection(object): 14 | 15 | X = 0 16 | Y = 1 17 | Z = 2 18 | 19 | def __init__(self, scene, linkFrom, moveDir, appDim, linkTo): 20 | object.__init__(self) 21 | self.scene = scene 22 | self.linkFrom = linkFrom 23 | self.linkFrom.posChanged.connect(self.position) 24 | self.linkTo = linkTo 25 | self.linkTo.posChanged.connect(self.position) 26 | self.appDim = appDim 27 | self.moveDir = moveDir 28 | self.skin = None 29 | self.margin = 10 30 | 31 | def __del__(self): 32 | self.linkFrom.posChanged.disconnect(self.position) 33 | self.linkTo.posChanged.disconnect(self.position) 34 | if self.skin: 35 | del self.skin 36 | 37 | def __str__(self): 38 | return "({0}, {1})".format(self.linkFrom, self.linkTo) 39 | 40 | def remove(self, cached=False): 41 | if not self.skin: 42 | return 43 | self.scene.removeItem(self.skin) 44 | if not cached: 45 | self.skin = None 46 | 47 | # Case 1 X1 < X2, Case 2 X1 > X2 48 | def positionYRankWithDifferentX(self, path, fromRect, toRect): 49 | if fromRect.center().x() < toRect.center().x(): 50 | s, t = fromRect, toRect 51 | moveDir = self.moveDir 52 | else: 53 | s, t = toRect, fromRect 54 | moveDir = Cell.invertDir(self.moveDir) 55 | if moveDir == Cell.NEG: 56 | startY, endY = s.top(), t.bottom() 57 | sControlY, tControlY = startY-(s.height()/2), endY+(t.height()/2) 58 | else: 59 | startY, endY = s.bottom(), t.top() 60 | sControlY, tControlY = startY+(s.height()/2), endY-(t.height()/2) 61 | 62 | # Source is always left of target 63 | path.moveTo(s.center().x(), startY) 64 | sp = QPointF(s.left()+s.width(), sControlY) 65 | tp = QPointF(t.right()-t.width(), tControlY) 66 | end = QPointF(t.center().x(), endY) 67 | path.cubicTo(sp, tp, end) 68 | 69 | # Case 1 Y1 < Y2, Case 2 Y1 > Y2 70 | def positionXRankWithDifferentY(self, path, fromRect, toRect): 71 | if fromRect.center().y() > toRect.center().y(): 72 | s, t = fromRect, toRect 73 | moveDir = self.moveDir 74 | else: 75 | s, t = toRect, fromRect 76 | moveDir = Cell.invertDir(self.moveDir) 77 | sw, sh = s.width(), s.height() 78 | tw, th = t.width(), t.height() 79 | if moveDir == Cell.NEG: 80 | startX, endX = s.left(), t.right() 81 | sControlX = startX-(sw); tControlX = endX-(tw) 82 | else: 83 | startX, endX = s.right(), t.left() 84 | sControlX = startX+(sw); tControlX = endX-(tw) 85 | 86 | # Source is always below target 87 | path.moveTo(startX, s.center().y()) 88 | sp = QPointF(sControlX, s.top()-(sh*2)) 89 | tp = QPointF(tControlX, t.bottom()+(th*2)) 90 | end = QPointF(endX, t.center().y()) 91 | path.cubicTo(sp, tp, end) 92 | 93 | 94 | # Case 3 X1 == X2 95 | def positionYRingRank(self, path, fromRect, toRect): 96 | if fromRect.center().y() < toRect.center().y(): 97 | s, t = fromRect, toRect 98 | moveDir = self.moveDir 99 | else: 100 | s, t = toRect, fromRect 101 | moveDir = Cell.invertDir(self.moveDir) 102 | rad = max(s.width()/3, t.width()/3) 103 | if moveDir == Cell.NEG: 104 | p = QPointF(s.center().x(), s.top())+QPointF(0, -rad) 105 | leftBox = QtCore.QRectF(p, QSizeF(rad+rad,rad+rad)) 106 | p = QPointF(s.center().x(), t.bottom())+QPointF(0, -rad) 107 | rightBox = QtCore.QRectF(p, QSizeF(rad+rad,rad+rad)) 108 | else: 109 | p = QPointF(s.center().x(), s.bottom())+QPointF(0, rad) 110 | leftBox = QtCore.QRectF(p, QSizeF(rad+rad,rad+rad)) 111 | p = QPointF(s.center().x(), t.top())+QPointF(0, -rad) 112 | rightBox = QtCore.QRectF(p, QSizeF(rad+rad,rad+rad)) 113 | # Source always above target 114 | path.moveTo(leftBox.left(), s.top()) 115 | path.arcTo(leftBox, -180, -180) 116 | path.lineTo(rightBox.right(), t.bottom()) 117 | path.arcTo(rightBox, 0, -180) 118 | 119 | # Case 3 Y1 == Y2 120 | def positionXRingRank(self, path, fromRect, toRect): 121 | if fromRect.center().x() < toRect.center().x(): 122 | s, t = fromRect, toRect 123 | moveDir = self.moveDir 124 | else: 125 | s, t = toRect, fromRect 126 | moveDir = Cell.invertDir(self.moveDir) 127 | rad = max(s.height()/3, t.height()/3) 128 | if moveDir == Cell.NEG: 129 | p = QPointF(s.left(), s.center().y())+QPointF(-(rad), -(rad*2)) 130 | leftBox = QtCore.QRectF(p, QSizeF(rad*2,rad*2)) 131 | p = QPointF(t.right(), s.center().y())+QPointF(-(rad), -(rad*2)) 132 | rightBox = QtCore.QRectF(p, QSizeF(rad*2,rad*2)) 133 | else: 134 | p = QPointF(s.right(), s.center().y())+QPointF(-rad, -rad) 135 | leftBox = QtCore.QRectF(p, QSizeF(rad+rad,rad+rad)) 136 | p = QPointF(t.left(), s.center().y())+QPointF(-rad, -rad) 137 | rightBox = QtCore.QRectF(p, QSizeF(rad+rad,rad+rad)) 138 | # Source always left of target 139 | path.moveTo(s.left(), s.center().y()) 140 | path.arcTo(leftBox, -90, -180) 141 | path.lineTo(rightBox.topLeft()) 142 | path.arcTo(rightBox, 90, -180) 143 | 144 | @pyqtSlot() 145 | def position(self): 146 | if not self.skin: 147 | self.skin = QtGui.QGraphicsPathItem() 148 | self.scene.addItem(self.skin) 149 | self.skin.setZValue(0) 150 | self.skin.setPen(QPen(QColor("black"), 1)) 151 | adjRect = self.linkFrom.skin.sceneBoundingRect() 152 | newRect = self.linkTo.skin.sceneBoundingRect() 153 | path = QtGui.QPainterPath() 154 | if self.appDim == self.X: 155 | yDiff = int(abs(adjRect.center().y() - newRect.center().y())) 156 | if self.moveDir == self.linkTo.POS: 157 | fromX, toX = adjRect.right(), newRect.left() 158 | order = fromX < toX # adjCell - newCell 159 | else: 160 | fromX, toX = adjRect.left(), newRect.right() 161 | order = fromX > toX # newCell - adjCell 162 | if yDiff < self.margin and order: # Case 0 163 | path.moveTo(fromX, adjRect.center().y()) 164 | path.lineTo(toX, newRect.center().y()) 165 | elif not (yDiff < self.margin): # Case 1, 2 166 | self.positionXRankWithDifferentY(path, adjRect, newRect) 167 | else: # Case 3 168 | self.positionXRingRank(path, adjRect, newRect) 169 | elif self.appDim == self.Y: 170 | xDiff = int(abs(adjRect.center().x() - newRect.center().x())) 171 | if self.moveDir == self.linkFrom.NEG: 172 | fromY, toY = adjRect.top(), newRect.bottom() 173 | comp = fromY > toY 174 | else: 175 | fromY, toY = adjRect.bottom(), newRect.top() 176 | comp = fromY < toY 177 | if xDiff < self.margin and comp: # Case 0 178 | path.moveTo(adjRect.center().x(), fromY) 179 | path.lineTo(newRect.center().x(), toY) 180 | elif not (xDiff < self.margin): # Case 1, 2 181 | self.positionYRankWithDifferentX(path, adjRect, newRect) 182 | else: # Case 3 183 | self.positionYRingRank(path, adjRect, newRect) 184 | self.skin.setPath(path) 185 | 186 | -------------------------------------------------------------------------------- /dimscape/windowUI.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file 'main_window.ui' 4 | # 5 | # Created: Tue Jul 5 00:28:38 2011 6 | # by: PyQt4 UI code generator 4.8.4 7 | # 8 | # WARNING! All changes made in this file will be lost! 9 | 10 | from PyQt4 import QtCore, QtGui 11 | from dimview import DimView 12 | 13 | try: 14 | _fromUtf8 = QtCore.QString.fromUtf8 15 | except AttributeError: 16 | _fromUtf8 = lambda s: s 17 | 18 | class Ui_MainWindow(object): 19 | def setupUi(self, MainWindow): 20 | MainWindow.setObjectName(_fromUtf8("MainWindow")) 21 | MainWindow.resize(800, 600) 22 | sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) 23 | sizePolicy.setHorizontalStretch(0) 24 | sizePolicy.setVerticalStretch(0) 25 | sizePolicy.setHeightForWidth(MainWindow.sizePolicy().hasHeightForWidth()) 26 | MainWindow.setSizePolicy(sizePolicy) 27 | self.centralwidget = QtGui.QWidget(MainWindow) 28 | sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) 29 | sizePolicy.setHorizontalStretch(0) 30 | sizePolicy.setVerticalStretch(0) 31 | sizePolicy.setHeightForWidth(self.centralwidget.sizePolicy().hasHeightForWidth()) 32 | self.centralwidget.setSizePolicy(sizePolicy) 33 | self.centralwidget.setObjectName(_fromUtf8("centralwidget")) 34 | self.verticalLayout = QtGui.QVBoxLayout(self.centralwidget) 35 | self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) 36 | self.dimscapeView = DimView(self.centralwidget) 37 | self.dimscapeView.setObjectName(_fromUtf8("dimscapeView")) 38 | self.verticalLayout.addWidget(self.dimscapeView) 39 | spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) 40 | self.verticalLayout.addItem(spacerItem) 41 | MainWindow.setCentralWidget(self.centralwidget) 42 | self.menubar = QtGui.QMenuBar(MainWindow) 43 | self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 24)) 44 | self.menubar.setObjectName(_fromUtf8("menubar")) 45 | self.menuFile = QtGui.QMenu(self.menubar) 46 | self.menuFile.setAccessibleName(_fromUtf8("")) 47 | self.menuFile.setObjectName(_fromUtf8("menuFile")) 48 | self.menuDimension = QtGui.QMenu(self.menubar) 49 | self.menuDimension.setObjectName(_fromUtf8("menuDimension")) 50 | self.menuCell = QtGui.QMenu(self.menubar) 51 | self.menuCell.setObjectName(_fromUtf8("menuCell")) 52 | MainWindow.setMenuBar(self.menubar) 53 | self.statusbar = QtGui.QStatusBar(MainWindow) 54 | self.statusbar.setObjectName(_fromUtf8("statusbar")) 55 | MainWindow.setStatusBar(self.statusbar) 56 | self.toolBar = QtGui.QToolBar(MainWindow) 57 | self.toolBar.setObjectName(_fromUtf8("toolBar")) 58 | MainWindow.addToolBar(QtCore.Qt.TopToolBarArea, self.toolBar) 59 | self.actionNew_Space = QtGui.QAction(MainWindow) 60 | self.actionNew_Space.setObjectName(_fromUtf8("actionNew_Space")) 61 | self.actionLoad_Space = QtGui.QAction(MainWindow) 62 | self.actionLoad_Space.setObjectName(_fromUtf8("actionLoad_Space")) 63 | self.actionQuit = QtGui.QAction(MainWindow) 64 | self.actionQuit.setObjectName(_fromUtf8("actionQuit")) 65 | self.actionX = QtGui.QAction(MainWindow) 66 | self.actionX.setObjectName(_fromUtf8("actionX")) 67 | self.actionY = QtGui.QAction(MainWindow) 68 | self.actionY.setObjectName(_fromUtf8("actionY")) 69 | self.actionZ = QtGui.QAction(MainWindow) 70 | self.actionZ.setObjectName(_fromUtf8("actionZ")) 71 | self.actionEdit_Cell = QtGui.QAction(MainWindow) 72 | self.actionEdit_Cell.setObjectName(_fromUtf8("actionEdit_Cell")) 73 | self.actionInsert_Cell = QtGui.QAction(MainWindow) 74 | self.actionInsert_Cell.setObjectName(_fromUtf8("actionInsert_Cell")) 75 | self.actionDelete_Cell = QtGui.QAction(MainWindow) 76 | self.actionDelete_Cell.setObjectName(_fromUtf8("actionDelete_Cell")) 77 | self.actionSave_Space = QtGui.QAction(MainWindow) 78 | self.actionSave_Space.setObjectName(_fromUtf8("actionSave_Space")) 79 | self.actionSave_Space_As = QtGui.QAction(MainWindow) 80 | self.actionSave_Space_As.setObjectName(_fromUtf8("actionSave_Space_As")) 81 | self.actionNew_Dim = QtGui.QAction(MainWindow) 82 | self.actionNew_Dim.setObjectName(_fromUtf8("actionNew_Dim")) 83 | self.actionClose = QtGui.QAction(MainWindow) 84 | self.actionClose.setObjectName(_fromUtf8("actionClose")) 85 | self.menuFile.addAction(self.actionNew_Space) 86 | self.menuFile.addAction(self.actionLoad_Space) 87 | self.menuFile.addSeparator() 88 | self.menuFile.addAction(self.actionSave_Space) 89 | self.menuFile.addAction(self.actionSave_Space_As) 90 | self.menuFile.addSeparator() 91 | self.menuFile.addAction(self.actionClose) 92 | self.menuFile.addAction(self.actionQuit) 93 | self.menuDimension.addAction(self.actionNew_Dim) 94 | self.menuCell.addAction(self.actionEdit_Cell) 95 | self.menuCell.addAction(self.actionInsert_Cell) 96 | self.menuCell.addAction(self.actionDelete_Cell) 97 | self.menubar.addAction(self.menuFile.menuAction()) 98 | self.menubar.addAction(self.menuDimension.menuAction()) 99 | self.menubar.addAction(self.menuCell.menuAction()) 100 | 101 | self.retranslateUi(MainWindow) 102 | QtCore.QMetaObject.connectSlotsByName(MainWindow) 103 | 104 | def retranslateUi(self, MainWindow): 105 | MainWindow.setWindowTitle(QtGui.QApplication.translate("MainWindow", "Dimscape", None, QtGui.QApplication.UnicodeUTF8)) 106 | self.menuFile.setTitle(QtGui.QApplication.translate("MainWindow", "&File", None, QtGui.QApplication.UnicodeUTF8)) 107 | self.menuDimension.setTitle(QtGui.QApplication.translate("MainWindow", "&Dimension", None, QtGui.QApplication.UnicodeUTF8)) 108 | self.menuCell.setTitle(QtGui.QApplication.translate("MainWindow", "&Cell", None, QtGui.QApplication.UnicodeUTF8)) 109 | self.toolBar.setWindowTitle(QtGui.QApplication.translate("MainWindow", "toolBar", None, QtGui.QApplication.UnicodeUTF8)) 110 | self.actionNew_Space.setText(QtGui.QApplication.translate("MainWindow", "&New Space", None, QtGui.QApplication.UnicodeUTF8)) 111 | self.actionNew_Space.setShortcut(QtGui.QApplication.translate("MainWindow", "Ctrl+N", None, QtGui.QApplication.UnicodeUTF8)) 112 | self.actionLoad_Space.setText(QtGui.QApplication.translate("MainWindow", "&Open Space", None, QtGui.QApplication.UnicodeUTF8)) 113 | self.actionLoad_Space.setShortcut(QtGui.QApplication.translate("MainWindow", "Ctrl+L", None, QtGui.QApplication.UnicodeUTF8)) 114 | self.actionQuit.setText(QtGui.QApplication.translate("MainWindow", "&Quit", None, QtGui.QApplication.UnicodeUTF8)) 115 | self.actionQuit.setShortcut(QtGui.QApplication.translate("MainWindow", "Ctrl+Q", None, QtGui.QApplication.UnicodeUTF8)) 116 | self.actionX.setText(QtGui.QApplication.translate("MainWindow", "X", None, QtGui.QApplication.UnicodeUTF8)) 117 | self.actionY.setText(QtGui.QApplication.translate("MainWindow", "Y", None, QtGui.QApplication.UnicodeUTF8)) 118 | self.actionZ.setText(QtGui.QApplication.translate("MainWindow", "Z", None, QtGui.QApplication.UnicodeUTF8)) 119 | self.actionEdit_Cell.setText(QtGui.QApplication.translate("MainWindow", "Edit Cell", None, QtGui.QApplication.UnicodeUTF8)) 120 | self.actionInsert_Cell.setText(QtGui.QApplication.translate("MainWindow", "Insert Cell", None, QtGui.QApplication.UnicodeUTF8)) 121 | self.actionDelete_Cell.setText(QtGui.QApplication.translate("MainWindow", "Delete Cell", None, QtGui.QApplication.UnicodeUTF8)) 122 | self.actionSave_Space.setText(QtGui.QApplication.translate("MainWindow", "&Save Space", None, QtGui.QApplication.UnicodeUTF8)) 123 | self.actionSave_Space.setShortcut(QtGui.QApplication.translate("MainWindow", "Ctrl+S", None, QtGui.QApplication.UnicodeUTF8)) 124 | self.actionSave_Space_As.setText(QtGui.QApplication.translate("MainWindow", "Save Space &As", None, QtGui.QApplication.UnicodeUTF8)) 125 | self.actionNew_Dim.setText(QtGui.QApplication.translate("MainWindow", "New Dimension", None, QtGui.QApplication.UnicodeUTF8)) 126 | self.actionNew_Dim.setShortcut(QtGui.QApplication.translate("MainWindow", "Ctrl+D", None, QtGui.QApplication.UnicodeUTF8)) 127 | self.actionClose.setText(QtGui.QApplication.translate("MainWindow", "Close", None, QtGui.QApplication.UnicodeUTF8)) 128 | 129 | -------------------------------------------------------------------------------- /dimscape/types/cell.py: -------------------------------------------------------------------------------- 1 | from __future__ import generators, print_function 2 | # -*- coding: utf-8 -*- 3 | 4 | from PyQt4.QtCore import QObject, pyqtSignal, pyqtSlot, QRectF, \ 5 | QPropertyAnimation, pyqtProperty, QPointF, QSizeF 6 | from PyQt4.QtGui import QBrush, QPen, QColor, QGraphicsObject, \ 7 | QGraphicsItem, QAbstractGraphicsShapeItem 8 | 9 | import logging 10 | dlog = logging.getLogger("dimscape.types") 11 | 12 | class Cell(QObject): 13 | 14 | NEG = 0 15 | POS = 1 16 | 17 | def __init__(self, cellId, data, cons=None, props=None): 18 | QObject.__init__(self) 19 | self._cellId = cellId 20 | self._data = data 21 | self.cons = {} 22 | if cons: 23 | self.addCons(cons) 24 | self.dataInline = True 25 | self.transient = False 26 | if props and "file-backed" in props: 27 | self.dataInline = not props["file-backed"] 28 | 29 | # do this so that subclasses (Text, etc.) can propertyize at will 30 | @property 31 | def data(self): 32 | return self._data 33 | @data.setter 34 | def data(self, val): 35 | self._data = val 36 | 37 | def isTransient(self): 38 | return self.transient 39 | 40 | def setTransient(self, trans=None): 41 | if trans == None: self.transient = True 42 | else: self.transient = trans 43 | 44 | @classmethod 45 | def invertDir(cls, direc): 46 | if direc == cls.NEG: 47 | return cls.POS 48 | return cls.NEG 49 | 50 | def removeCons(self, dims, repair=True): 51 | for dim in dims: 52 | self.removeCon(dim, repair) 53 | 54 | def removeCon(self, dim, repair=True, direction=None): 55 | if direction != None: 56 | if direction == self.NEG: 57 | self.removeNegCon(dim, repair) 58 | else: 59 | self.removePosCon(dim, repair) 60 | return 61 | d = self.cons.pop(dim, None) 62 | if d: 63 | neg_cell, pos_cell = d[0], d[1] 64 | if None != neg_cell and None != pos_cell: 65 | if repair: 66 | neg_cell.addPosCon(dim, pos_cell, replace=True) 67 | pos_cell.addNegCon(dim, neg_cell, replace=True) 68 | else: 69 | neg_cell.removePosCon(dim) 70 | pos_cell.removeNegCon(dim) 71 | elif None != neg_cell: 72 | neg_cell.removePosCon(dim) 73 | elif None != pos_cell: 74 | pos_cell.removeNegCon(dim) 75 | 76 | def removeNegCon(self, dim, repair=True): 77 | if dim in self.cons: 78 | d = self.cons[dim] 79 | if d[0] and repair: 80 | d[0].removePosCon(dim, repair=False) 81 | self.cons[dim] = (None, d[1]) 82 | if self.cons[dim] == (None, None): 83 | self.cons.pop(dim) 84 | 85 | def removePosCon(self, dim, repair=True): 86 | if dim in self.cons: 87 | d = self.cons[dim] 88 | if d[1] and repair: 89 | d[1].removeNegCon(dim, repair=False) 90 | self.cons[dim] = (d[0], None) 91 | if self.cons[dim] == (None, None): 92 | self.cons.pop(dim) 93 | 94 | def addCons(self, dim_dict): 95 | for k,v in dim_dict.iteritems(): 96 | self.addCon(k, v) 97 | 98 | def addCon(self, dim, direcs, replace=False): 99 | self.addNegCon(dim, direcs[0], replace) 100 | self.addPosCon(dim, direcs[1], replace) 101 | 102 | def addNegCon(self, dim, neg_cell, replace=False): 103 | d = self.cons.setdefault(dim, (None,None)) 104 | if None != d[0] and not replace: 105 | d[0].addPosCon(dim, neg_cell, replace=True) 106 | neg_cell.addNegCon(dim, d[0], replace=True) 107 | self.cons[dim] = (neg_cell, d[1]) 108 | 109 | def addPosCon(self, dim, pos_cell, replace=False): 110 | d = self.cons.setdefault(dim, (None,None)) 111 | if None != d[1] and not replace: 112 | d[1].addNegCon(dim, pos_cell, replace=True) 113 | pos_cell.addPosCon(dim, d[1], replace=True) 114 | self.cons[dim] = (d[0], pos_cell) 115 | 116 | def hasCon(self, dim, direc=None): 117 | if dim in self.cons: 118 | if direc in [self.NEG, self.POS]: 119 | return (self.cons[dim][direc] != None) 120 | return True 121 | return False 122 | 123 | def isConnectedTo(self, cell): 124 | for (n, p) in self.cons.itervalues(): 125 | if n == cell or p == cell: 126 | return True 127 | return False 128 | 129 | def unlink(self, repair=True): 130 | self.removeCons(list(self.cons.iterkeys()), repair) 131 | 132 | def hasCons(self): 133 | return (len(self.cons) != 0) 134 | 135 | def getCon(self, dim, direction): 136 | if self.cons[dim][direction] == None: 137 | return None 138 | return self.cons[dim][direction] 139 | 140 | # Property cellId 141 | @property 142 | def cellId(self): 143 | return self._cellId 144 | @cellId.setter 145 | def cellId(self, oth): 146 | if (oth < 0): 147 | raise ValueError("Cell id cannot be less than zero.") 148 | self._cellId = oth 149 | 150 | def execute(self): 151 | pass 152 | 153 | def edit(self): 154 | pass 155 | 156 | class Skin(QGraphicsObject): 157 | def __init__(self, parent=None): 158 | QGraphicsObject.__init__(self, parent) 159 | self.margin = 5 160 | self._pen = QPen() 161 | self._brush = QBrush() 162 | self._targetPos = self.pos() 163 | 164 | def targetPos(self): 165 | return self._targetPos 166 | def setTargetPos(self, pos): 167 | self._targetPos = pos 168 | 169 | def pen(self): return self._pen 170 | def setPen(self, pen): self._pen = pen 171 | 172 | def brush(self): return self._brush 173 | def setBrush(self, brush): self._brush = brush 174 | 175 | def rect(self): return self.boundingRect() 176 | def setRect(self, rect): 177 | pass 178 | 179 | def boundingRect(self): 180 | # For now, do not take the highlight pen into account 181 | #pw = self.pen().widthF()/2 182 | pw = 0 183 | children = self.childrenBoundingRect() 184 | m = self.margin 185 | return QRectF(children.topLeft()-QPointF(pw+m, pw+m), 186 | QSizeF(children.width()+pw+m+m, children.height()+pw+m+m)) 187 | 188 | def paint(self, painter, options, widget): 189 | pw = self.pen().widthF()/2 190 | children = self.childrenBoundingRect() 191 | m = self.margin 192 | painter.setPen(self.pen()) 193 | painter.setBrush(self.brush()) 194 | painter.drawRect(QRectF(children.topLeft()-QPointF(pw+m, pw+m), 195 | QSizeF(children.width()+pw+m+m, children.height()+pw+m+m))) 196 | 197 | 198 | 199 | class CellSkin(Cell): 200 | 201 | sel_brush = QBrush(QColor("cyan")) 202 | sel_pen = QPen(QBrush(QColor("black")), 3) 203 | posChanged = pyqtSignal() 204 | 205 | def __init__(self, cid, data, cons=None, props=None): 206 | Cell.__init__(self, cid, data, cons, props) 207 | self.skin = None 208 | self.old_brush = None 209 | self.old_pen = None 210 | self.loaded = False 211 | self.initialMove = True 212 | self.animation = QPropertyAnimation() 213 | 214 | # Called when we can't find the attribute, probably going 215 | # to skin 216 | def __getattr__(self, name): 217 | if name in self.__dict__: 218 | return self.__dict__[name] 219 | return self.__dict__["skin"].__getattribute__(name) 220 | 221 | def __del__(self): 222 | if self.skin: 223 | del self.skin 224 | 225 | def getChild(self, num=0): 226 | childs = self.skin.childItems() 227 | if self.skin and num >= 0 and \ 228 | num < len(childs): 229 | return childs[num] 230 | return None 231 | 232 | def getSkin(self): 233 | return self.skin 234 | 235 | def add(self, space): 236 | if not self.loaded: 237 | if not self.skin: 238 | self.skin = Skin() 239 | self.skin.xChanged.connect(self.posChanged) 240 | self.skin.yChanged.connect(self.posChanged) 241 | self.skin.zChanged.connect(self.posChanged) 242 | self.skin.setPen(QPen(QBrush(QColor("black")), 1)) 243 | self.skin.setBrush(QColor("tan")) 244 | dlog.debug("adding skin for first time: " + str(self.skin)) 245 | space.scene.addItem(self.getSkin()) 246 | self.placeChildren(space) 247 | self.updateRect() 248 | self.skin.setZValue(2) 249 | self.initialMove = True 250 | else: 251 | dlog.debug ("adding item: " + str(self.getSkin())) 252 | space.scene.addItem(self.getSkin()) 253 | self.loaded = True 254 | 255 | @pyqtSlot() 256 | def updateRect(self): 257 | if self.skin: 258 | self.skin.setRect(self.skin.childrenBoundingRect()) 259 | 260 | def setPos(self, center): 261 | if self.skin: 262 | # setPos works in terms of topLeft, but center point is 263 | # easiest on the frontend, so convert 264 | rect = self.getSkin().sceneBoundingRect() 265 | topLeft = QPointF(center[0] - rect.width()/2, 266 | center[1] - rect.height()/2) 267 | if self.initialMove: 268 | self.skin.setPos(topLeft) 269 | self.skin.setTargetPos(topLeft) 270 | self.initialMove = False 271 | else: 272 | self.animation.stop() 273 | while self.animation.state() != self.animation.Stopped: 274 | pass 275 | self.animation.setTargetObject(self.skin) 276 | self.animation.setPropertyName("pos") 277 | self.animation.setDuration(1000) 278 | self.animation.setEndValue(topLeft) 279 | self.skin.setTargetPos(topLeft) 280 | self.animation.start() 281 | 282 | def remove(self, scene, cached=True): 283 | if not self.loaded: 284 | return 285 | scene.removeItem(self.getSkin()) 286 | self.loaded = False 287 | if not cached: 288 | self.skin = None 289 | 290 | def select(self): 291 | # subclasses can play with these, so save them 292 | self.old_brush = self.skin.brush() 293 | self.old_pen = self.skin.pen() 294 | self.skin.setBrush(self.sel_brush) 295 | self.skin.setPen(self.sel_pen) 296 | self.skin.setZValue(8) 297 | 298 | def deselect(self): 299 | if self.old_brush: 300 | self.skin.setBrush(self.old_brush) 301 | self.skin.setPen(self.old_pen) 302 | self.skin.setZValue(2) 303 | -------------------------------------------------------------------------------- /bin/dimscape: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import generators, print_function 4 | 5 | import os, sys 6 | 7 | # Import Qt modules 8 | from PyQt4 import QtCore, QtGui 9 | from PyQt4.QtCore import pyqtSlot 10 | 11 | from getopt import gnu_getopt 12 | import threading 13 | 14 | from dimscape.windowUI import Ui_MainWindow 15 | from dimscape.space import DimSpace 16 | from dimscape import ops 17 | from dimscape.log import initLog 18 | import logging as l 19 | 20 | spaces = [] 21 | 22 | # Create a class for our main window 23 | class Main(QtGui.QMainWindow): 24 | 25 | NormalMode = 0 26 | CommandMode = 1 27 | InsertMode = 2 28 | 29 | modeStrs = [ "", ":", "Insert" ] 30 | 31 | def __init__(self): 32 | QtGui.QMainWindow.__init__(self) 33 | 34 | if not hasattr(Main, "numOpened"): 35 | Main.numOpened = 0 36 | Main.numOpened += 1 37 | 38 | # This is always the same 39 | self.ui=Ui_MainWindow() 40 | self.ui.setupUi(self) 41 | self.cells = [] 42 | 43 | self.scene=QtGui.QGraphicsScene() 44 | self.scene.setSceneRect(0,0,10000,10000) 45 | self.ui.dimscapeView.setScene(self.scene) 46 | self.ui.dimscapeView.setSceneRect(2000,2000,8000,8000) 47 | self.ui.dimscapeView.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) 48 | self.ui.dimscapeView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) 49 | #self.ui.dimscapeView.setViewport(QtOpenGL.QGLWidget()) 50 | 51 | self.setWindowTitle(self.tr("Untitled %1").arg( 52 | self.numOpened)) 53 | self.setWindowIcon(QtGui.QIcon("dimscape.png")) 54 | 55 | self.space = DimSpace(self.scene) 56 | self.space.dimChanged.connect(self.dimDisplayUpdate) 57 | self.dimItems = [] 58 | # Open in new window 59 | self.ui.actionNew_Space.triggered.connect(self.newSpace) 60 | # Close window 61 | self.ui.actionClose.triggered.connect(self.close) 62 | # Open an existing space 63 | self.ui.actionLoad_Space.triggered.connect(self.openSpace) 64 | # Save curFileName 65 | self.ui.actionSave_Space.triggered.connect(self.save) 66 | # Save to a new file and work from there 67 | self.ui.actionSave_Space_As.triggered.connect(self.saveAs) 68 | self.ui.actionQuit.triggered.connect( 69 | QtGui.QApplication.quit) 70 | 71 | self.curFileName = None 72 | self.currentMode = self.NormalMode 73 | self.operations = [] 74 | self.activeMenu = None 75 | self.msgArea = self.scene.addText("") 76 | 77 | self.setWindowState(QtCore.Qt.WindowMaximized) 78 | 79 | @pyqtSlot(int, str) 80 | def dimDisplayUpdate(self, appDim, boundDim): 81 | t = str(self.dimItems[appDim].text()) 82 | self.dimItems[appDim].setText(t[:3] + boundDim) 83 | 84 | def enterMode(self, mode): 85 | self.currentMode = mode 86 | self.msgArea.setPlainText(self.modeStrs[mode]) 87 | 88 | def processNormalKeys(self, k, mods): 89 | qt = QtCore.Qt 90 | self.msgArea.setPlainText("") 91 | if k == qt.Key_R: 92 | rop = ops.UnlinkCellOperation(self.space, 93 | self.space.acursedCell, 94 | self.msgArea, allLinks=True) 95 | rop.show() 96 | self.operations.append(rop) 97 | elif k == qt.Key_D: 98 | dop = ops.NewDimOperation(self.space, 99 | self.space.acursedCell, 100 | self.msgArea) 101 | dop.show() 102 | self.operations.append(dop) 103 | elif k == qt.Key_L: 104 | if mods == qt.ShiftModifier: 105 | lop = ops.UnlinkCellOperation(self.space, 106 | self.space.acursedCell, self.msgArea, 107 | allLinks=False) 108 | else: 109 | lop = ops.LinkOperation(self.space, 110 | self.space.acursedCell, 111 | self.msgArea) 112 | lop.show() 113 | self.operations.append(lop) 114 | elif k == qt.Key_C: 115 | ncop = ops.NewCellOperation(self.space, 116 | self.space.acursedCell, 117 | self.msgArea) 118 | ncop.show() 119 | self.operations.append(ncop) 120 | elif k == qt.Key_S: 121 | self.space.swapDims() 122 | self.space.redraw() 123 | elif k == qt.Key_X: 124 | if mods == qt.ShiftModifier: 125 | self.space.chugDim(self.space.NEG, self.space.X) 126 | else: 127 | self.space.chugDim(self.space.POS, self.space.X) 128 | self.space.redraw() 129 | elif k == qt.Key_Y: 130 | if mods == qt.ShiftModifier: 131 | self.space.chugDim(self.space.NEG, self.space.Y) 132 | else: 133 | self.space.chugDim(self.space.POS, self.space.Y) 134 | self.space.redraw() 135 | elif k == qt.Key_Z: 136 | if mods == qt.ShiftModifier: 137 | self.space.chugDim(self.space.NEG, self.space.Z) 138 | else: 139 | self.space.chugDim(self.space.POS, self.space.Z) 140 | self.space.redraw() 141 | elif k == qt.Key_Colon: 142 | self.enterMode(self.CommandMode) 143 | elif k == qt.Key_I: 144 | self.enterMode(self.InsertMode) 145 | self.space.editCell() 146 | elif k == qt.Key_Enter or k == qt.Key_Return: 147 | self.space.executeCell() 148 | elif k == qt.Key_Up: 149 | # has to just be Ctrl-Up, in case somebody wants 150 | # Ctrl-Shift-Up for something 151 | # eventually all will be ported to generic key-bindings 152 | # .ds.bindingName / .ds.boundKeys 153 | if mods == qt.ControlModifier: 154 | if self.space.chug(self.space.NEG, self.space.Z): 155 | self.space.chugDraw() 156 | else: 157 | if self.space.chug(self.space.NEG, self.space.Y): 158 | self.space.chugDraw() 159 | elif k == qt.Key_Down: 160 | if mods == qt.ControlModifier: 161 | if self.space.chug(self.space.POS, self.space.Z): 162 | self.space.chugDraw() 163 | else: 164 | if self.space.chug(self.space.POS, self.space.Y): 165 | self.space.chugDraw() 166 | elif k == qt.Key_Right: 167 | if self.space.chug(self.space.POS, self.space.X): 168 | self.space.chugDraw() 169 | elif k == qt.Key_Left: 170 | if self.space.chug(self.space.NEG, self.space.X): 171 | self.space.chugDraw() 172 | 173 | def processInsertKeys(self, k, mods): 174 | if k == QtCore.Qt.Key_Escape: 175 | self.scene.setFocusItem(None) 176 | self.enterMode(self.NormalMode) 177 | return True 178 | return False 179 | 180 | def execCommand(self): 181 | pass 182 | 183 | def processCommandKeys(self, k, mods): 184 | qt = QtCore.Qt 185 | if k == qt.Key_Escape: 186 | self.enterMode(self.NormalMode) 187 | elif k == qt.Key_Enter or k == qt.Key_Return: 188 | self.execCommand() 189 | 190 | def keyPressEvent(self, evt): 191 | if not self.space.back: 192 | QtGui.QWidget.keyPressEvent(self, evt) 193 | return 194 | k = evt.key() 195 | mods = evt.modifiers() 196 | qt = QtCore.Qt 197 | 198 | if self.currentMode == self.InsertMode: 199 | it = self.scene.focusItem() 200 | if not self.processInsertKeys(k, mods) and it: 201 | self.scene.sendEvent(it, evt) 202 | else: 203 | return 204 | if self.currentMode == self.CommandMode: 205 | self.processCommandKeys(k, mods) 206 | return 207 | 208 | count = len(self.operations)-1 209 | while count >= 0: 210 | op = self.operations[count] 211 | if op.isFinished(): 212 | self.operations.pop() 213 | elif not op.isFinished() and op.processKeys(k, mods): 214 | return 215 | count -= 1 216 | 217 | if self.currentMode == self.NormalMode: 218 | self.processNormalKeys(k, mods) 219 | 220 | @staticmethod 221 | @pyqtSlot() 222 | def newSpace(): 223 | win = Main() 224 | spaces.append(win) 225 | win.show() 226 | win.openNewSpace() 227 | 228 | def __del__(self): 229 | if spaces and self in spaces: 230 | spaces.remove(self) 231 | 232 | @pyqtSlot() 233 | def save(self): 234 | self.ui.actionSave_Space.setEnabled(False) 235 | if self.curFileName: 236 | self.space.save() 237 | else: 238 | self.saveAs() 239 | 240 | @pyqtSlot() 241 | def saveAs(self): 242 | self.curFileName = QtGui.QFileDialog.getSaveFileName(self, 243 | self.tr("Save DimSpace File As"), 244 | str(QtCore.QDir.homePath()) + os.sep + self.windowTitle() + ".ds", 245 | self.tr("DimScape Files (*.ds)")) 246 | if self.curFileName: 247 | l.getLogger("dimscape").info("Saving to file: %s", 248 | self.curFileName) 249 | self.space.saveAs(str(self.curFileName)) 250 | self.ui.actionSave_Space.setEnabled(False) 251 | self.setWindowTitle(self.curFileName) 252 | 253 | @pyqtSlot() 254 | def openSpace(self, path=None): 255 | if path: 256 | self.curFileName = path 257 | else: 258 | self.curFileName = QtGui.QFileDialog.getOpenFileName(self, 259 | self.tr("Open DimSpace File"), 260 | QtCore.QDir.homePath(), 261 | self.tr("DimScape Files (*.ds)")) 262 | if self.curFileName: 263 | l.getLogger("dimscape").info("Opening file: %s", 264 | self.curFileName) 265 | self.initSpace(self.curFileName) 266 | 267 | def openNewSpace(self): 268 | l.getLogger("dimscape").info("Opening a new space") 269 | self.initSpace() 270 | 271 | def initSpace(self, fileName=None): 272 | self.cancelOps() 273 | self.ui.actionSave_Space.setEnabled(True) 274 | center = self.ui.dimscapeView.mapToScene( 275 | self.ui.dimscapeView.viewport().rect().center()) 276 | if not fileName: 277 | self.space.load((center.x(), center.y())) 278 | else: 279 | self.space.load((center.x(), center.y()), fileName) 280 | # setup ui 281 | self.msgArea.setPos( 282 | self.ui.dimscapeView.mapToScene(0, 100)) 283 | yAccum = 0 284 | dimLabels = ["Z: ", "Y: ", "X: "] 285 | for d in self.space.dims[:3]: 286 | l = dimLabels.pop() 287 | self.dimItems.append(self.scene.addSimpleText(l + d)) 288 | self.dimItems[-1].setPos( 289 | self.ui.dimscapeView.mapToScene(0, yAccum)) 290 | yAccum += self.dimItems[-1].boundingRect().height() + 2 291 | 292 | def cancelOps(self): 293 | dlog = l.getLogger("dimscape") 294 | for op in self.operations: 295 | op.cancel() 296 | self.operations = [] 297 | 298 | def main(): 299 | #sys.setrecursionlimit(100) 300 | # 64k stack limit, as from what I hear, linux uses 8M normally 301 | threading.stack_size(64*1024) 302 | app = QtGui.QApplication(sys.argv) 303 | app.setApplicationName("Dimscape") 304 | 305 | #locale = QtCore.QLocale.system().name() 306 | #trans = QtCore.QTranslator() 307 | #trans.load(":ts/dimscape_" + locale) 308 | #app.installTranslator(trans) 309 | 310 | levels = [l.WARN, l.INFO, l.DEBUG] 311 | logLevel = 0 312 | 313 | flags, positionals = gnu_getopt(sys.argv, "hv", ["help"]) 314 | positionals.pop(0) # get rid of argv[0] 315 | 316 | for (flag, val) in flags: 317 | if flag == "-h" or flag == "--help": 318 | usage() 319 | sys.exit() 320 | elif flag == '-v': 321 | # allows to increase verbosity, -v, -vv, -vvv 322 | if (logLevel+1) < len(levels): logLevel += 1 323 | 324 | # Get our root logger set up 325 | initLog(levels[logLevel]) 326 | 327 | # the root logger identifies as 'root', which is confusing, so 328 | # get our own named logger 329 | dlog = l.getLogger("dimscape") 330 | 331 | for arg in positionals: 332 | if os.path.exists(arg): 333 | win = Main() 334 | spaces.append(win) 335 | win.show() 336 | win.openSpace(arg) 337 | else: 338 | dlog.error("File could not be found: %s", arg) 339 | 340 | # If we were unable to load anything, or the command-line was empty 341 | # show something 342 | if not spaces: 343 | Main.newSpace() 344 | 345 | # It's exec_ because exec is a reserved word in Python 346 | sys.exit(app.exec_()) 347 | 348 | 349 | if __name__ == "__main__": 350 | main() 351 | -------------------------------------------------------------------------------- /dimscape/space.py: -------------------------------------------------------------------------------- 1 | from __future__ import generators, print_function 2 | # -*- coding: utf-8 -*- 3 | 4 | from PyQt4 import QtCore, QtGui 5 | from PyQt4.phonon import Phonon 6 | from PyQt4.QtCore import pyqtSlot, pyqtSignal, QPointF 7 | 8 | from jsonBackend import DJSONBackend 9 | from dimscape.types import CellTypeRegistrar 10 | from dimscape.types.cell import Cell 11 | from dimscape.connection import Connection 12 | 13 | import logging 14 | dlog = logging.getLogger("dimscape.space") 15 | 16 | class DimSpace(QtCore.QObject): 17 | NEG = Cell.NEG 18 | POS = Cell.POS 19 | 20 | X=0 21 | Y=1 22 | Z=2 23 | 24 | dimChanged = pyqtSignal(int, str) 25 | reg = CellTypeRegistrar.get() 26 | 27 | def __init__(self, scene): 28 | QtCore.QObject.__init__(self) 29 | self.scene = scene 30 | self.back = None 31 | self.connections = [] 32 | self.reg.dynamicCellsRegistered.connect(self.updateDynamicallyTypedCells) 33 | 34 | @pyqtSlot(list) 35 | def updateDynamicallyTypedCells(self, cells): 36 | for c in cells: 37 | old_c = self.cells[c.cellId] 38 | if self.acursedCell == old_c: 39 | self.setAcursed(c) 40 | old_c.remove(self.space, cached=False) 41 | self.cells[c.cellId] = c 42 | self.redraw() 43 | 44 | def save(self): 45 | # self.dims always points to the latest dims 46 | self.back.dimConfig = self.dims 47 | # we maintain the acursedCell, the backends maintains the 48 | # acursedId, the two need only meet at save time 49 | self.back.acursedId = self.acursedCell.cellId 50 | self.back.save() 51 | 52 | def saveAs(self, path): 53 | filey = file(path, "w") 54 | self.back.saveAs(filey) 55 | 56 | def load(self, origin, path=None): 57 | if self.back: 58 | self.clear() 59 | if path: 60 | filey = file(path, "r") 61 | self.back = DJSONBackend(filey) 62 | else: 63 | rootCell = self.reg.fromName("text")(0, "Root") 64 | self.back = DJSONBackend.createNew(rootCell) 65 | self.acursedCell = self.back.cells[self.back.acursedId] 66 | # needs to update the backend's version at save time 67 | self.dims = self.back.dimConfig 68 | self.dimsStack = [ self.dims ] 69 | # just pointers to the backend's structures, no need to update 70 | self.allDims = self.back.allDims 71 | self.cells = self.back.cells 72 | self.origin = origin 73 | self.redraw() 74 | self.acursedCell.select() 75 | 76 | def swapDims(self): 77 | self.dims[0], self.dims[1] = self.dims[1], self.dims[0] 78 | self.dimChanged.emit(self.X, self.dims[0]) 79 | self.dimChanged.emit(self.Y, self.dims[1]) 80 | 81 | def nameDim(self, dim): 82 | if not dim in self.allDims: 83 | self.allDims.append(dim) 84 | 85 | def pushDims(self): 86 | self.dimsStack.append(list(self.dims)) 87 | self.dims = self.dimsStack[-1] 88 | 89 | def popDims(self): 90 | if len(self.dimsStack) > 1: 91 | othDims = self.dimsStack.pop() 92 | self.dims = self.dimsStack[-1] 93 | for i in xrange(len(self.dims)): 94 | if self.dims[i] != othDims[i]: 95 | self.dimChanged.emit(i, self.dims[i]) 96 | 97 | def setDim(self, appDim, boundDim): 98 | self.dims[appDim] = boundDim 99 | self.dimChanged.emit(appDim, boundDim) 100 | 101 | def getDim(self, appDim): 102 | return self.dims[appDim] 103 | 104 | def dirToString(self, direc): 105 | if direc == self.NEG: 106 | return "Negward" 107 | return "Posward" 108 | 109 | def dimToString(self, appDim): 110 | return chr(appDim + ord('X')) 111 | 112 | def removeCell(self, cell): 113 | cell.remove(self.scene) 114 | cell.unlink() 115 | if not cell.isTransient(): 116 | self.cells[cell.cellId] = None 117 | 118 | def removeLink(self, cell, appDim, direction=None): 119 | cell.removeCon(self.dims[appDim], repair=True, direction=direction) 120 | 121 | def link(self, linkFrom, moveDir, appDim, linkTo, 122 | exclusive=None): 123 | if exclusive: 124 | linkFrom.unlink(repair=False) 125 | if self.POS == moveDir: 126 | linkFrom.addPosCon(self.dims[appDim], linkTo) 127 | linkTo.addNegCon(self.dims[appDim], linkFrom) 128 | else: 129 | linkFrom.addNegCon(self.dims[appDim], linkTo) 130 | linkTo.addPosCon(self.dims[appDim], linkFrom) 131 | 132 | def makeTransientCell(self, theType, *args): 133 | # cellId is really only checked at save time now that 134 | # we use cells as connections for in memory cells 135 | # Since we ignore transient cells anyway, this id can 136 | # be any garbage value 137 | cell = theType(-1, *args) 138 | cell.setTransient() 139 | cell.sel_brush = QtGui.QBrush(QtGui.QColor("magenta")) 140 | return cell 141 | 142 | @staticmethod 143 | def makeCell(self, theType, *args): 144 | cell = theType(len(self.cells), *args) 145 | self.cells.append(cell) 146 | 147 | def makeCellConcrete(self, transCell): 148 | transCell.setTransient(False) 149 | transCell.cellId = len(self.cells) 150 | transCell.sel_brush = QtGui.QBrush(QtGui.QColor("cyan")) 151 | self.cells.append(transCell) 152 | 153 | def setAcursed(self, cell): 154 | self.acursedCell.deselect() 155 | cell.select() 156 | self.acursedCell = cell 157 | self.acursedId = cell.cellId 158 | 159 | def removeTransientCells(self, cell, prevCell=None, 160 | moveDir=None, dimen=None): 161 | if cell.isTransient(): 162 | # cannot unlink yet 163 | cell.remove(self.scene, cached=False) 164 | self.cells[cell.cellId] = None 165 | return True 166 | 167 | def chugDim(self, moveDir, appDim): 168 | dim = self.dims[appDim] 169 | if moveDir == self.NEG: direc = -1 170 | else: direc = 1 171 | print ("dims:", self.allDims) 172 | ind = (self.allDims.index(dim) + direc) % len(self.allDims) 173 | self.dims[appDim] = self.allDims[ind] 174 | self.dimChanged.emit(appDim, self.dims[appDim]) 175 | 176 | def redraw(self): 177 | """Call after dimensions are changed""" 178 | self.clear() 179 | self.broadcast(self.redrawCell, self.acursedCell) 180 | self.broadcast(self.drawCons, self.acursedCell, allCons=True) 181 | 182 | def fixOverlap(self, cell, prevCell=None, 183 | moveDir=None, dimen=None): 184 | if not prevCell: 185 | return 186 | self.placeRelativeTo(cell, prevCell, moveDir, dimen) 187 | 188 | def redrawCell(self, cell, prevCell=None, 189 | moveDir=None, dimen=None): 190 | if prevCell == None: 191 | cell.add(self) 192 | cell.setPos(self.origin) 193 | return 194 | self.placeRelativeTo(cell, prevCell, moveDir, dimen) 195 | 196 | def chugDraw(self): 197 | """No new cells to create, regroup cells around new acursed""" 198 | self.broadcast(self.chugDrawCell, self.acursedCell) 199 | if not self.connections and len(self.cells) > 1: 200 | self.broadcast(self.drawCons, self.acursedCell, allCons=True) 201 | 202 | def chugDrawCell(self, cell, prevCell=None, 203 | moveDir=None, dimen=None): 204 | if prevCell == None: 205 | cell.add(self) 206 | cell.setPos(self.origin) 207 | return 208 | self.placeRelativeTo(cell, prevCell, moveDir, dimen) 209 | 210 | def removeOverlap(self, cell, prevCell=None, 211 | moveDir=None, dimen=None): 212 | if prevCell == None: 213 | return 214 | items = cell.collidingItems(QtCore.Qt.IntersectsItemBoundingRect) 215 | if items: 216 | for item in items: 217 | if isinstance(item, QtGui.QGraphicsSimpleTextItem): 218 | inter = cell.rect().intersected(item.rect()) 219 | 220 | def drawCons(self, cell, prevCell=None, 221 | moveDir=None, dimen=None): 222 | if prevCell == None: 223 | return 224 | conMap = map(lambda x: (x.linkTo, x.linkFrom), self.connections) 225 | if (prevCell, cell) in conMap: 226 | return 227 | con = Connection(self.scene, prevCell, moveDir, dimen, cell) 228 | con.position() 229 | self.connections.append(con) 230 | 231 | def placeRelativeTo(self, cell, adjCell, moveDir, dimen): 232 | # TODO: Z will crash and burn atm 233 | cell.add(self) 234 | if adjCell == self.acursedCell: 235 | cell.setZValue(6) 236 | elif adjCell.isConnectedTo(self.acursedCell): 237 | cell.setZValue(4) 238 | newRect = QtCore.QRectF(cell.skin.sceneBoundingRect()) 239 | adjRect = adjCell.skin.sceneBoundingRect() 240 | adjRect.moveTopLeft(adjCell.skin.targetPos()) 241 | if dimen == self.X: 242 | if moveDir == self.NEG: 243 | newRect.moveCenter(QPointF( 244 | adjRect.left() - 10 - newRect.width()/2, 245 | adjRect.center().y())) 246 | else: 247 | newRect.moveCenter(QPointF( 248 | adjRect.right() + 10 + newRect.width()/2, 249 | adjRect.center().y())) 250 | elif dimen == self.Y: 251 | if moveDir == self.NEG: 252 | newRect.moveCenter(QPointF( 253 | adjRect.center().x(), 254 | adjRect.top() - 10 - newRect.height()/2)) 255 | else: 256 | newRect.moveCenter(QPointF( 257 | adjRect.center().x(), 258 | adjRect.bottom() + 10 + newRect.height()/2)) 259 | center = (newRect.center().x(), newRect.center().y()) 260 | cell.setPos(center) 261 | return True 262 | 263 | def colwiseTraversal(self, func, curCell, 264 | allCons=False, marked=None, moveDir=None): 265 | if not marked: 266 | marked = [ curCell ] 267 | func(curCell, None, None, None) 268 | if cell.hasCon(self.dims[self.X], self.NEG) and not \ 269 | self.POS == moveDir: 270 | cony = cell.getCon(self.dims[self.X], self.NEG) 271 | if cony not in marked: 272 | marked.append(cony) 273 | self.colwiseTraversal(func, cony, allCons, 274 | marked, self.NEG) 275 | elif allCons: 276 | func(cony, cell, self.NEG, self.X) 277 | if cell.hasCon(self.dims[self.X], self.POS) and not \ 278 | self.NEG == moveDir: 279 | cony = cell.getCon(self.dims[self.X], self.POS) 280 | if cony not in marked: 281 | marked.append(cony) 282 | self.colwiseTraversal(func, cony, allCons, 283 | marked, self.POS) 284 | elif allCons: 285 | func(cony, cell, self.POS, self.X) 286 | if cell.hasCon(self.dims[self.Y], self.NEG) and not \ 287 | self.POS == moveDir: 288 | cony = cell.getCon(self.dims[self.Y], self.NEG) 289 | if cony not in marked: 290 | marked.append(cony) 291 | self.colwiseTraversal(func, cony, allCons, 292 | marked, self.NEG) 293 | elif allCons: 294 | func(cony, cell, self.NEG, self.Y) 295 | if cell.hasCon(self.dims[self.Y], self.POS) and not \ 296 | self.NEG == moveDir: 297 | cony = cell.getCon(self.dims[self.Y], self.POS) 298 | if cony not in marked: 299 | marked.append(cony) 300 | self.colwiseTraversal(func, cony, allCons, 301 | marked, self.POS) 302 | elif allCons: 303 | func(cony, cell, self.POS, self.Y) 304 | 305 | def broadcast(self, func, curCell, allCons=False): 306 | #if not isinstance(curCell, list): 307 | #curCell = [ curCell ] 308 | #marked = [ curCell[0] ] 309 | #goCons = [ (cell, None, None, None) for cell in curCell ] 310 | marked = [ curCell ] 311 | goCons = [ (curCell, None, None, None) ] 312 | for (cell, prevCell, moveDir, dimen) in goCons: 313 | func(cell, prevCell, moveDir, dimen) 314 | for i in xrange(len(self.dims)): 315 | if cell.hasCon(self.dims[i], self.NEG) and not \ 316 | (i == dimen and self.POS == moveDir): 317 | cony = cell.getCon(self.dims[i], self.NEG) 318 | if cony not in marked: 319 | marked.append(cony) 320 | goCons.append((cony, cell, self.NEG, i)) 321 | elif allCons: 322 | func(cony, cell, self.NEG, i) 323 | if cell.hasCon(self.dims[i], self.POS) and not \ 324 | (i == dimen and self.NEG == moveDir): 325 | cony = cell.getCon(self.dims[i], self.POS) 326 | if cony not in marked: 327 | marked.append(cony) 328 | goCons.append((cony, cell, self.POS, i)) 329 | elif allCons: 330 | func(cony, cell, self.POS, i) 331 | 332 | def executeCell(self): 333 | print ("executing cell:", self.acursedCell, 334 | self.acursedCell.getChild()) 335 | self.acursedCell.execute() 336 | 337 | def editCell(self): 338 | self.acursedCell.edit() 339 | 340 | def clear(self): 341 | # Cache our cell skins in our cells 342 | for c in self.cells: 343 | if c: 344 | c.remove(self.scene) 345 | # Clear connections from scene 346 | for con in self.connections: 347 | con.remove() 348 | self.connections = [] 349 | 350 | def chug(self, direction, apparentDim): 351 | if apparentDim < len(self.dims) and apparentDim >= 0: 352 | curDim = self.dims[apparentDim] 353 | if self.acursedCell.hasCon(curDim, direction): 354 | self.setAcursed(self.acursedCell.getCon(curDim, 355 | direction)) 356 | return True 357 | return False 358 | 359 | def chugWhile(self, count, direc, dim): 360 | if callable(count): 361 | while count(self.acursedCell) and self.chug(direc, dim): 362 | pass 363 | else: 364 | while count >= 0 and chug(direc, dim): 365 | count -= 1 366 | 367 | --------------------------------------------------------------------------------