diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..48f1dfae --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +htmleditor/*.js linguist-vendored diff --git a/CHANGELOG.md b/CHANGELOG.md index 79945eed..f83f91a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ This file documents any relevant changes done to ViUR Vi since version 2. This is the current development version. +## [2.5.0] Vesuv + +Release date: Jul 26, 2019 + +- Feature: Support for new recordBone() +- Bugfix: Improved `textBone.unserialize()` and change-event behavior +- Bugfix: Widget for `stringBone(multiple=True)` is now cleared on unserialization +- Bugfix: Added missing `serializeForDocument()` to spatialBone +- Bugfix: SelectMultiBoneExtractor.render() works now correctly for `selectBone(multiple=True)` +- Bugfix: Disallow applying SelectFieldsPopup with empty selection (#27) +- Bugfix: Selection in SelectTable works now correctly +- Bugfix: On server version mismatch, allow to continue anyway + ## [2.4.1] Agung Release date: May 24, 2019 @@ -111,7 +124,8 @@ Release date: Dec 22, 2016 - Styling -[develop]: https://github.com/viur-framework/vi/compare/v2.4.1...develop +[develop]: https://github.com/viur-framework/vi/compare/v2.5.0...develop +[2.5.0]: https://github.com/viur-framework/vi/compare/v2.4.1...v2.5.0 [2.4.1]: https://github.com/viur-framework/vi/compare/v2.4.0...v2.4.1 [2.4.0]: https://github.com/viur-framework/vi/compare/v2.3.0...v2.4.0 [2.3.0]: https://github.com/viur-framework/vi/compare/v2.2.0...v2.3.0 diff --git a/actions/list.py b/actions/list.py index 268b4c0e..1b1bd367 100644 --- a/actions/list.py +++ b/actions/list.py @@ -494,14 +494,20 @@ def __init__(self, listWdg, *args, **kwargs): self.appendChild(self.cancelBtn) def doApply(self, *args, **kwargs): - self.applyBtn["class"].append("is_loading") - self.applyBtn["disabled"] = True - res = [] for c in self.checkboxes: if c["checked"]: res.append( c["value"] ) + if not res: + html5.ext.Alert( + translate("You have to select at least on field to continue!") + ) + return + + self.applyBtn["class"].append("is_loading") + self.applyBtn["disabled"] = True + self.listWdg.setFields( res ) self.close() diff --git a/bones/__init__.py b/bones/__init__.py index 42059796..d6418776 100644 --- a/bones/__init__.py +++ b/bones/__init__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from bones import base from bones import relational +from bones import record from bones import file from bones import hierarchy from bones import selectmulti diff --git a/bones/record.py b/bones/record.py new file mode 100644 index 00000000..7949654d --- /dev/null +++ b/bones/record.py @@ -0,0 +1,469 @@ +# -*- coding: utf-8 -*- +import html5, json, utils +from bones.base import BaseBoneExtractor +from config import conf +from event import EventDispatcher +from i18n import translate +from priorityqueue import editBoneSelector, viewDelegateSelector, extractorDelegateSelector +from widgets.edit import InternalEdit + + +class RecordBoneExtractor(BaseBoneExtractor): + def __init__(self, module, boneName, skelStructure): + super(RecordBoneExtractor, self).__init__(module, boneName, skelStructure) + self.format = "$(dest.name)" + + if "format" in skelStructure[boneName]: + self.format = skelStructure[boneName]["format"] + + def render(self, data, field): + assert field == self.boneName, "render() was called with field %s, expected %s" % (field, self.boneName) + + val = data.get(field) + if not field: + return "" + + structure = self.skelStructure[self.boneName] + + try: + if not isinstance(val, list): + val = [val or ""] + + val = ", ".join([utils.formatString(self.format, x, structure["using"], language=conf["currentlanguage"]) + for x in val]) + except: + # We probably received some garbage + print("%s: RecordBoneExtractor.render cannot build relational format, maybe garbage received?" % self.boneName) + print(val) + val = "" + + return val + + def raw(self, data, field): + assert field == self.boneName, "render() was called with field %s, expected %s" % (field, self.boneName) + + val = data.get(field) + if not val: + return None + + structure = self.skelStructure[self.boneName] + + try: + if not isinstance(val, list): + val = [val] + + val = ", ".join([utils.formatString(self.format, x, structure["using"], language=conf["currentlanguage"]) + for x in val]) + except: + # We probably received some garbage + print("%s: RecordBoneExtractor.raw cannot build relational format, maybe garbage received?" % self.boneName) + print(val) + return None + + return val[0] if len(val) == 1 else val + + +class RecordViewBoneDelegate(object): + + def __init__(self, module, boneName, structure): + super(RecordViewBoneDelegate, self).__init__() + self.module = module + self.structure = structure + self.boneName = boneName + + if "format" in structure[boneName]: + self.format = structure[boneName]["format"] + + def render(self, data, field): + assert field == self.boneName, "render() was called with field %s, expected %s" % (field, self.boneName) + val = data.get(field) + + lbl = html5.Label() + + if val is None: + lbl.appendChild(conf["empty_value"]) + return lbl + + structure = self.structure[self.boneName] + + try: + if not isinstance(val, list): + val = [val] + count = 1 + else: + count = len(val) + if conf["maxMultiBoneEntries"] and count >= conf["maxMultiBoneEntries"]: + val = val[:conf["maxMultiBoneEntries"] - 1] + + res = "\n".join([utils.formatString(self.format, x, structure["using"], language=conf["currentlanguage"]) + for x in val]) + + if conf["maxMultiBoneEntries"] and count >= conf["maxMultiBoneEntries"]: + res += "\n%s" % translate("and {count} more", count=count - conf["maxMultiBoneEntries"] - 1) + + except: + # We probably received some garbage + print( + "%s: RecordViewBoneDelegate.render cannot build relational format, maybe garbage received?" % self.boneName) + print(val) + + res = "" + + html5.utils.textToHtml(lbl, html5.utils.unescape(res)) + return lbl + + +class RecordSingleBone(html5.Div): + """ + Provides the widget for a recordBone with multiple=False + """ + + def __init__(self, moduleName, boneName, using, readOnly, required, *args, **kwargs): + super(RecordSingleBone, self).__init__(*args, **kwargs) + + self.addClass("recordbone", "recordbone-single") + + self.moduleName = moduleName + self.boneName = boneName + self.readOnly = readOnly + self.required = required + self.using = using + + self.mask = None + + self.changeEvent = EventDispatcher("boneChange") + + if self.readOnly: + self["disabled"] = True + + def _setDisabled(self, disable): + super(RecordSingleBone, self)._setDisabled(disable) + if not disable and not self._disabledState: + self.parent().removeClass("is_active") + + @classmethod + def fromSkelStructure(cls, moduleName, boneName, skelStructure, *args, **kwargs): + readOnly = skelStructure[boneName].get("readonly", False) + required = skelStructure[boneName].get("required", False) + using = skelStructure[boneName]["using"] + + return cls(moduleName, boneName, using, readOnly, required, ) + + def unserialize(self, data): + if self.boneName in data: + val = data[self.boneName] + if isinstance(val, list): + if len(val) > 0: + val = val[0] + else: + val = None + + if not isinstance(val, dict): + val = {} + + if self.mask: + self.removeChild(self.mask) + + self.mask = InternalEdit(self.using, val, {}, readOnly=self.readOnly, defaultCat=None) + self.appendChild(self.mask) + + def serializeForPost(self): + res = self.mask.serializeForPost() + return {"%s.%s" % (self.boneName, k): v for (k, v) in res.items()} + + def serializeForDocument(self): + return {self.boneName: self.mask.serializeForDocument()} + + @staticmethod + def checkFor(moduleName, boneName, skelStructure, *args, **kwargs): + isMultiple = "multiple" in skelStructure[boneName] and skelStructure[boneName]["multiple"] + return not isMultiple and (skelStructure[boneName]["type"] == "record" + or skelStructure[boneName]["type"].startswith("record.")) + + +class RecordMultiBoneEntry(html5.Div): + """ + Wrapper-class that holds one referenced entry in a RecordMultiBone. + Provides the UI to display its data and a button to remove it from the bone. + """ + + def __init__(self, parent, module, data, using, errorInfo=None, *args, **kwargs): + super(RecordMultiBoneEntry, self).__init__(*args, **kwargs) + self.sinkEvent("onDrop", "onDragOver", "onDragLeave", "onDragStart", "onDragEnd", "onChange") + + self.addClass("recordbone-entry") + + self.parent = parent + self.module = module + self.data = data + + self.mask = InternalEdit(using, data, errorInfo, readOnly=parent.readOnly, defaultCat=None) + self.appendChild(self.mask) + + if not parent.readOnly: + remBtn = html5.ext.Button(translate("Remove"), self.onRemove) + remBtn["class"].append("icon") + remBtn["class"].append("cancel") + self.appendChild(remBtn) + + def onDragStart(self, event): + if self.parent.readOnly: + return + + self.addClass("is-dragging") + + self.parent.currentDrag = self + event.dataTransfer.setData("application/json", json.dumps(self.data)) + event.stopPropagation() + + def onDragOver(self, event): + if self.parent.readOnly: + return + + if self.parent.currentDrag is not self: + self.addClass("is-dragging-over") + self.parent.currentOver = self + + event.preventDefault() + + def onDragLeave(self, event): + if self.parent.readOnly: + return + + self.removeClass("is-dragging-over") + self.parent.currentOver = None + + event.preventDefault() + + def onDragEnd(self, event): + if self.parent.readOnly: + return + + self.removeClass("is-dragging") + self.parent.currentDrag = None + + if self.parent.currentOver: + self.parent.currentOver.removeClass("is-dragging-over") + self.parent.currentOver = None + + event.stopPropagation() + + def onDrop(self, event): + if self.parent.readOnly: + return + + event.preventDefault() + event.stopPropagation() + + if self.parent.currentDrag and self.parent.currentDrag != self: + if self.element.offsetTop > self.parent.currentDrag.element.offsetTop: + if self.parent.entries[-1] is self: + self.parent.moveEntry(self.parent.currentDrag) + else: + self.parent.moveEntry(self.parent.currentDrag, + self.parent.entries[self.parent.entries.index(self) + 1]) + else: + self.parent.moveEntry(self.parent.currentDrag, self) + + self.parent.currentDrag = None + + def onChange(self, event): + data = self.data.copy() + data["rel"].update(self.ie.doSave()) + + self.updateLabel(data) + + def onRemove(self, *args, **kwargs): + self.parent.removeEntry(self) + self.parent.changeEvent.fire(self.parent) + + def serializeForPost(self): + return self.mask.serializeForPost() + + def serializeForDocument(self): + return self.mask.serializeForDocument() + + +class RecordMultiBone(html5.Div): + """ + Provides the widget for a recordBone with multiple=True + """ + + def __init__(self, moduleName, boneName, readOnly, using, *args, **kwargs): + super(RecordMultiBone, self).__init__(*args, **kwargs) + + self.addClass("recordbone", "recordbone-multi") + + self.moduleName = moduleName + self.boneName = boneName + self.readOnly = readOnly + self.using = using + + self.changeEvent = EventDispatcher("boneChange") + + self.entries = [] + self.extendedErrorInformation = {} + self.currentDrag = None + self.currentOver = None + + self.itemsDiv = html5.Div() + self.itemsDiv.addClass("recordbone-entries") + self.appendChild(self.itemsDiv) + + self.addBtn = html5.ext.Button("Add", self.onAddBtnClick) + self.addBtn.addClass("icon", "add") + self.appendChild(self.addBtn) + + if self.readOnly: + self["disabled"] = True + + self.sinkEvent("onDragOver") + + def _setDisabled(self, disable): + """ + Reset the is_active flag (if any) + """ + super(RecordMultiBone, self)._setDisabled(disable) + if not disable and not self._disabledState: + self.parent().removeClass("is_active") + + @classmethod + def fromSkelStructure(cls, moduleName, boneName, skelStructure, *args, **kwargs): + """ + Constructs a new RecordMultiBone from the parameters given in skelStructure. + @param moduleName: Name of the module which send us the skelStructure + @type moduleName: string + @param boneName: Name of the bone which we shall handle + @type boneName: string + @param skelStructure: The parsed skeleton structure send by the server + @type skelStructure: dict + """ + readOnly = bool(skelStructure[boneName].get("readonly", False)) + using = skelStructure[boneName]["using"] + + return cls(moduleName, boneName, readOnly, using) + + def unserialize(self, data): + """ + Parses the values received from the server and update our value accordingly. + @param data: The data dictionary received. + @type data: dict + """ + self.itemsDiv.removeAllChildren() + self.entries = [] + + if self.boneName in data: + val = data[self.boneName] + if isinstance(val, dict): + val = [val] + + self.setContent(val) + + def serializeForPost(self): + res = {} + + for idx, entry in enumerate(self.entries): + currRes = entry.serializeForPost() + + for k, v in currRes.items(): + res["%s.%d.%s" % (self.boneName, idx, k)] = v + + if not res: + res[self.boneName] = None + + return res + + def serializeForDocument(self): + return {self.boneName: [entry.serializeForDocument() for entry in self.entries]} + + def setContent(self, content): + """ + Set our current value to 'selection' + @param selection: The new entry that this bone should reference + @type selection: dict + """ + if content is None: + return + + for data in content: + errIdx = len(self.entries) + errDict = {} + + if self.extendedErrorInformation: + for k, v in self.extendedErrorInformation.items(): + k = k.replace("%s." % self.boneName, "") + if 1: + idx, errKey = k.split(".") + idx = int(idx) + else: + continue + + if idx == errIdx: + errDict[errKey] = v + + self.addEntry(RecordMultiBoneEntry(self, self.moduleName, data, self.using, errDict)) + + def onAddBtnClick(self, sender=None): + self.addEntry(RecordMultiBoneEntry(self, self.moduleName, {}, self.using)) + + def addEntry(self, entry): + """ + Adds a new RecordMultiBoneEntry to this bone. + @type entry: RecordMultiBoneEntry + """ + assert entry not in self.entries, "Entry %s is already in relationalBone" % str(entry) + self.entries.append(entry) + self.itemsDiv.appendChild(entry) + + def removeEntry(self, entry): + """ + Removes a RecordMultiBoneEntry from this bone. + @type entry: RecordMultiBoneEntry + """ + assert entry in self.entries, "Cannot remove unknown entry %s from relationalBone" % str(entry) + self.itemsDiv.removeChild(entry) + self.entries.remove(entry) + + def moveEntry(self, entry, before=None): + assert entry in self.entries, "Cannot remove unknown entry %s from relationalBone" % str(entry) + self.entries.remove(entry) + + if before: + assert before in self.entries, "Cannot remove unknown entry %s from relationalBone" % str(before) + self.itemsDiv.insertBefore(entry, before) + self.entries.insert(self.entries.index(before), entry) + else: + self.addEntry(entry) + + def setExtendedErrorInformation(self, errorInfo): + print("------- EXTENDEND ERROR INFO --------") + print(errorInfo) + self.extendedErrorInformation = errorInfo + for k, v in errorInfo.items(): + k = k.replace("%s." % self.boneName, "") + idx, err = k.split(".") + idx = int(idx) + + print("k: %s, v: %s" % (k, v)) + print("idx: %s" % idx) + print(len(self.entries)) + if idx >= 0 and idx < len(self.entries): + self.entries[idx].setError(err) + pass + + @staticmethod + def checkFor(moduleName, boneName, skelStructure, *args, **kwargs): + return skelStructure[boneName].get("multiple") and ( + skelStructure[boneName]["type"] == "record" + or skelStructure[boneName]["type"].startswith("record.")) + + +def checkForRecordBone(moduleName, boneName, skelStructure, *args, **kwargs): + return skelStructure[boneName]["type"] == "record" or skelStructure[boneName]["type"].startswith("record.") + + +# Register this Bone in the global queue +editBoneSelector.insert(5, RecordMultiBone.checkFor, RecordMultiBone) +editBoneSelector.insert(5, RecordSingleBone.checkFor, RecordSingleBone) +viewDelegateSelector.insert(5, checkForRecordBone, RecordViewBoneDelegate) +extractorDelegateSelector.insert(4, checkForRecordBone, RecordBoneExtractor) diff --git a/bones/selectmulti.py b/bones/selectmulti.py index 64c4338b..3010249b 100644 --- a/bones/selectmulti.py +++ b/bones/selectmulti.py @@ -10,19 +10,20 @@ class SelectMultiBoneExtractor(BaseBoneExtractor): def render(self, data, field): if field in data.keys(): + options = {k: v for k, v in self.skelStructure[field]["values"]} result = list() for fieldKey in data[field]: - if not fieldKey in self.skelStructure[field]["values"].keys(): + if not fieldKey in options.keys(): result.append(fieldKey) else: - value = self.skelStructure[field]["values"][fieldKey] + value = options.get(fieldKey) if value: result.append(value) return ",".join(result) - return conf[ "empty_value" ] + return conf["empty_value"] class SelectMultiViewBoneDelegate( object ): def __init__(self, moduleName, boneName, skelStructure, *args, **kwargs ): diff --git a/bones/spatial.py b/bones/spatial.py index 2c1eeb09..b1a19593 100644 --- a/bones/spatial.py +++ b/bones/spatial.py @@ -30,10 +30,15 @@ def fromSkelStructure(moduleName, boneName, skelStructure, *args, **kwargs): return SpatialBone(moduleName, boneName, readOnly) def unserialize(self, data): + if self.boneName not in data: + return + try: self.latitude["value"], self.longitude["value"] = data[self.boneName] except KeyError: pass + except TypeError: + pass def serializeForPost(self): return { @@ -41,13 +46,15 @@ def serializeForPost(self): "{0}.lng".format(self.boneName): self.longitude["value"] } + def serializeForDocument(self): + return self.serializeForPost() + def setExtendedErrorInformation(self, errorInfo): pass def CheckForSpatialBone(moduleName, boneName, skelStucture, *args, **kwargs): - tmp = str(skelStucture[boneName]["type"]).startswith("spatial") - return tmp + return skelStucture[boneName]["type"] == "spatial" or skelStucture[boneName]["type"].startswith("spatial.") # Register this Bone in the global queue diff --git a/bones/string.py b/bones/string.py index 5905c9a9..3a460411 100644 --- a/bones/string.py +++ b/bones/string.py @@ -329,10 +329,7 @@ def onBtnGenTag(self, btn): tag.focus() def unserialize(self, data, extendedErrorInformation=None): - if not self.boneName in data.keys(): - return - - data = data[self.boneName] + data = data.get(self.boneName) if not data: return @@ -340,6 +337,9 @@ def unserialize(self, data, extendedErrorInformation=None): assert isinstance(data, dict) for lang in self.languages: + for child in self.langEdits[lang].children(): + if isinstance(child, Tag): + self.langEdits[lang].removeChild(child) if lang in data.keys(): val = data[lang] @@ -361,6 +361,10 @@ def unserialize(self, data, extendedErrorInformation=None): elif not self.languages and self.multiple: + for child in self.tagContainer.children(): + if isinstance(child, Tag): + self.tagContainer.removeChild(child) + if isinstance(data, list): for tagStr in data: self.genTag(html5.utils.unescape(tagStr)) diff --git a/bones/text.py b/bones/text.py index 919ee980..9332f339 100644 --- a/bones/text.py +++ b/bones/text.py @@ -151,18 +151,22 @@ def fromSkelStructure(moduleName, boneName, skelStructure, *args, **kwargs): return TextEditBone(moduleName, boneName, readOnly, isPlainText, langs, descrHint=descr) def unserialize(self, data): + if self.boneName not in data: + return + self.valuesdict.clear() - if self.boneName in data.keys(): - if self.languages: - for lang in self.languages: - if self.boneName in data.keys() and isinstance(data[self.boneName], dict) and lang in data[ - self.boneName].keys(): - self.valuesdict[lang] = data[self.boneName][lang] - else: - self.valuesdict[lang] = "" - self.input["value"] = self.valuesdict[self.selectedLang] - else: - self.input["value"] = data[self.boneName] if data[self.boneName] else "" + data = data[self.boneName] or "" + + if self.languages: + for lang in self.languages: + if isinstance(data, dict): + self.valuesdict[lang] = data.get(lang, "") + else: + self.valuesdict[lang] = "" + + self.input["value"] = self.valuesdict[self.selectedLang] + else: + self.input["value"] = data def serializeForPost(self): if self.selectedLang: @@ -184,7 +188,7 @@ def onKeyUp(self, event): if self._changeTimeout: html5.window.clearTimeout(self._changeTimeout) - self._changeTimeout = html5.window.setTimeout(lambda: self.changeEvent.fire(self), 2500) + self._changeTimeout = html5.window.setTimeout(lambda: self.changeEvent.fire(self), 500) @staticmethod def checkForTextBone(moduleName, boneName, skelStucture, *args, **kwargs): diff --git a/config.py b/config.py index 8f4e732f..eedf43c0 100644 --- a/config.py +++ b/config.py @@ -5,7 +5,7 @@ conf = { # Vi version number - "vi.version": (2, 4, 1), + "vi.version": (2, 5, 0), # Appendix to version "vi.version.appendix": "", diff --git a/html5 b/html5 index 1671c75b..a803a435 160000 --- a/html5 +++ b/html5 @@ -1 +1 @@ -Subproject commit 1671c75b7fa04dd39a60691525404a97c583f90e +Subproject commit a803a435dc26fdc505ac35fb88f70236215624bb diff --git a/logics b/logics index 2cd8868c..f645a742 160000 --- a/logics +++ b/logics @@ -1 +1 @@ -Subproject commit 2cd8868cca34b0ec5e67af49353d1eb7dbe9187d +Subproject commit f645a742c7efd40a5587743e7c5acb74d68df278 diff --git a/main.py b/main.py index 7693045d..ed0f90e8 100644 --- a/main.py +++ b/main.py @@ -49,14 +49,13 @@ def getVersionSuccess(self, req): "vi.version": ".".join(str(x) for x in conf["vi.version"]), } - conf["server.version"] = None - html5.ext.Alert( translate("The ViUR server (v{server.version}) is incompatible to this Vi (v{vi.version}).", **params) + + "\n" + translate("There may be a lack on functionality.") + "\n" + translate("Please update either your server or Vi!"), title=translate("Version mismatch"), okCallback=self.startup, - okLabel=translate("Retry") + okLabel=translate("Continue anyway") ) return diff --git a/public/vi.less b/public/vi.less index 34d9af1b..6598f8c7 100644 --- a/public/vi.less +++ b/public/vi.less @@ -1547,6 +1547,38 @@ input[type="password"], .extendedrelational button.icon.select {float:left;} +/* recordBone +================================================== */ + +.recordbone-entries { + clear: both; +} +.recordbone-entry { + border: 3px dotted #ccc; + margin-bottom: 20px; + padding: 15px; +} + +.recordbone-entry .bone_show_composer { + overflow: hidden; + margin-bottom: 10px; +} + +.recordbone-entry .bone.str { + margin-bottom: 10px; +} + +.recordbone-entry .bone.text { + margin-bottom: 20px; + margin-top: 20px; +} + +form .recordbone-entry label.is_valid { + border: 1px solid #ccc; + border-right-width: 0; + background: #ddd; + color: #333; +} /* #vi_texteditor ================================================== */ diff --git a/translations/de.py b/translations/de.py index 9b681b0e..315707bc 100644 --- a/translations/de.py +++ b/translations/de.py @@ -153,10 +153,11 @@ "login with google": u"Mit Google einloggen", "the viur server (v{server.version}) is incompatible to this vi (v{vi.version}).": - u"Die Version des verwendeten ViUR servers (v{server.version}) ist inkompatibel mit diesem Vi (v{vi.version}).", - "please update either your server or vi!": u"Bitte aktualisieren Sie entweder den Server oder das Vi!", + u"Die Version des verwendeten ViUR Servers (v{server.version}) ist inkompatibel mit diesem Vi (v{vi.version}).", + "there may be a lack on functionality.": u"Diese Inkompatibilität kann zu technischen Problemen führen.", + "please update either your server or vi!": u"Bitte aktualisieren Sie entweder den Server oder das Vi auf eine neuere Version!", "version mismatch": u"Versionen stimmen nicht überein", - "retry": u"Nochmal versuchen", + "continue anyway": u"Trotzdem fortfahren", "vi.login.insufficient-rights": u"Der aktuelle angemeldete Benutzer hat nicht genügend Rechte um diese " u"Funktion zu verwenden.\n\nBitte loggen Sie sich unter einem anderen " u"Benutzer mit entsprechenden Zugriffsrechten ein.", diff --git a/widgets/table.py b/widgets/table.py index b0ce3e28..9fdc170d 100644 --- a/widgets/table.py +++ b/widgets/table.py @@ -156,15 +156,19 @@ def onMouseDown(self, event): if self._isCtlPressed: if row in self._selectedRows: + for x in self._selectedRows: + self.getTrByIndex(x).removeClass("is_focused") # remove focus self.removeSelectedRow( row ) else: self.addSelectedRow( row ) + self.setCursorRow(row, False) # set focus event.preventDefault() elif self._isShiftPressed: self.unSelectAll() for i in ( range(self._ctlStartRow, row+1) if self._ctlStartRow <= row else range(row, self._ctlStartRow+1) ): self.addSelectedRow( i ) + self.setCursorRow(row, False) # set focus event.preventDefault() elif self.checkboxes and html5.utils.doesEventHitWidgetOrChildren(event, self._checkboxes[row]): @@ -251,6 +255,8 @@ def onKeyDown(self, event): elif html5.isControl(event): # Ctrl self._isCtlPressed = True self._ctlStartRow = self._currentRow or 0 + if self._currentRow is not None: + self.addSelectedRow(self._currentRow) # add already selected row to selection elif html5.isShift(event): # Shift self._isShiftPressed = True @@ -264,6 +270,11 @@ def onKeyUp(self, event): self._isCtlPressed = False self._ctlStartRow = None + # leave selection mode if there is only one row selected and return to normal focus + if len(self._selectedRows) == 1: + for row in self.getCurrentSelection(): + self.removeSelectedRow(row) + elif html5.isShift(event): self._isShiftPressed = False self._ctlStartRow = None diff --git a/widgets/topbar.py b/widgets/topbar.py index ffe11b82..732d907f 100644 --- a/widgets/topbar.py +++ b/widgets/topbar.py @@ -20,9 +20,9 @@ def __init__(self): self.sinkEvent("onClick") - self.modulH1 = html5.H1() - self.modulH1._setClass("module") - self.appendChild(self.modulH1) + self.moduleH1 = html5.H1() + self.moduleH1._setClass("module") + self.appendChild(self.moduleH1) self.modulContainer = html5.Div() self.modulContainer["class"].append("currentmodul") @@ -47,13 +47,17 @@ def invoke(self): if widget: self.iconnav.appendChild(widget()) - def setTitle(self): - title = conf.get("vi.name") + def setTitle(self, title=None): + self.moduleH1.removeAllChildren() + + if title is None: + title = conf.get("vi.name") + if title: - self.modulH1.appendChild(html5.TextNode(html5.utils.unescape(title))) + self.moduleH1.appendChild(html5.TextNode(html5.utils.unescape(title))) def onClick(self, event): - if html5.utils.doesEventHitWidgetOrChildren(event, self.modulH1): + if html5.utils.doesEventHitWidgetOrChildren(event, self.moduleH1): conf["mainWindow"].switchFullscreen(not conf["mainWindow"].isFullscreen()) def setCurrentModulDescr(self, descr = "", iconURL=None, iconClasses=None, path=None):