[Merge] lp:~osomon/webbrowser-app/oxide-context-menu into lp:webbrowser-app
Olivier Tilloy
olivier.tilloy at canonical.com
Wed Aug 26 09:10:51 UTC 2015
Thanks for your thorough review. I replied all your comments inline (and updated the code where relevant). The only pending task is to update the code to use QMimeDatabase per your recommendation, which I’m working on now.
I removed the selection mechanism as part of this MR because in fact this new context menu implementation broke it even further (it had already been quite broken since its initial implementation, but this made things slightly worse, and I spent way too much time trying to fix it). In that regard it made sense to remove it now.
Diff comments:
>
> === modified file 'src/Ubuntu/Web/UbuntuWebView02.qml'
> --- src/Ubuntu/Web/UbuntuWebView02.qml 2015-08-13 19:06:11 +0000
> +++ src/Ubuntu/Web/UbuntuWebView02.qml 2015-08-25 15:14:26 +0000
> @@ -120,134 +89,72 @@
> }
>
> property var contextualActions // type: ActionList
> - Component {
> - id: contextualPopover
> - ActionSelectionPopover {
> - actions: contextualActions
> + contextMenu: ActionSelectionPopover {
This is the default context menu implementation for apps that embed a WebView. It’s a standard, unthemed ActionSelectionPopover. Apps can choose to override the contextMenu component if they want finer-grained control over the appearance and behaviour of the menu, but in the general case just specifying a bunch of actions with the 'contextualActions' property should be good enough.
Simply commenting out the 'contextMenu' definition in Browser.qml doesn’t work because the contextual actions’ enabled property depend on the contextModel property, which is being overridden in Browser.qml. If you also comment out that override, you will get a default popover context menu.
> + objectName: "contextMenu"
> + actions: contextualActions
> + caller: contextualRectangle
> + Component.onCompleted: {
> + internal.dismissCurrentContextualMenu()
> + internal.contextModel = model
> + var empty = true
> + if (actions) {
> + for (var i in actions.actions) {
> + if (actions.actions[i].enabled) {
> + empty = false
> + break
> + }
> + }
> + }
> + if (empty) {
> + internal.dismissCurrentContextualMenu()
> + } else {
> + contextualData.clear()
> + contextualData.href = model.linkUrl
> + contextualData.title = model.linkText
> + if ((model.mediaType == Oxide.WebView.MediaTypeImage) && model.hasImageContents) {
> + contextualData.img = model.srcUrl
> + }
> + show()
> + }
> + }
> + onVisibleChanged: {
> + if (!visible) {
> + internal.dismissCurrentContextualMenu()
> + }
> }
> }
> + readonly property QtObject contextModel: internal.contextModel
>
> property var selectionActions // type: ActionList
> - onSelectionActionsChanged: {
> - for (var i in selectionActions.actions) {
> - selectionActions.actions[i].onTriggered.connect(function () {
> - internal.dismissCurrentSelection()
> - })
> - }
> - }
> - Component {
> - id: selection
> - Selection {
> - anchors.fill: parent
> - property var mimedata: null
> - property rect bounds
> - onBoundsChanged: {
> - rect.x = bounds.x
> - rect.y = bounds.y
> - rect.width = bounds.width
> - rect.height = bounds.height
> - }
> - property Item actions: null
> - Component {
> - id: selectionPopover
> - ActionSelectionPopover {
> - objectName: "selectionActions"
> - autoClose: false
> - actions: selectionActions
> - }
> - }
> - function showActions() {
> - if (actions != null) {
> - actions.destroy()
> - }
> - actions = PopupUtils.open(selectionPopover, rect)
> - }
> - onResizingChanged: {
> - if (resizing) {
> - if (actions != null) {
> - actions.destroy()
> - }
> - }
> - }
> - onResized: {
> - var args = {x: rect.x, y: rect.y, width: rect.width, height: rect.height}
> - var msg = _webview.rootFrame.sendMessage("oxide://selection/", "adjustselection", args)
> - msg.onreply = function(response) {
> - internal.currentSelection.mimedata = internal.buildMimedata(response)
> - // Ensure that the bounds are updated
> - internal.currentSelection.bounds = Qt.rect(0, 0, 0, 0)
> - internal.currentSelection.bounds = internal.computeBounds(response)
> - internal.currentSelection.showActions()
> - }
> - msg.onerror = function(error) {
> - internal.dismissCurrentSelection()
> - }
> - }
> - onDismissed: internal.dismissCurrentSelection()
> - }
> - }
> + onSelectionActionsChanged: console.warn("WARNING: the 'selectionActions' property is deprecated and ignored.")
> function copy() {
> - if (internal.currentSelection != null) {
> - Clipboard.push(internal.currentSelection.mimedata)
> - } else {
> - console.warn("No current selection")
> - }
> + console.warn("WARNING: the copy() function is deprecated and does nothing.")
> }
>
> + readonly property real devicePixelRatio: internal.devicePixelRatio
> +
> QtObject {
> id: internal
> property int lastLoadRequestStatus: -1
> - property Item currentContextualMenu: null
> - property Item currentSelection: null
> -
> - function fillContextualData(data) {
> - contextualData.clear()
> - if ('img' in data) {
> - contextualData.img = data.img
> - }
> - if ('href' in data) {
> - contextualData.href = data.href
> - contextualData.title = data.title
> - }
> - }
> -
> - function buildMimedata(data) {
> - var mimedata = Clipboard.newData()
> - if ('html' in data) {
> - mimedata.html = data.html
> - }
> - // FIXME: push the text and image data in the order
> - // they appear in the selected block.
> - if ('text' in data) {
> - mimedata.text = data.text
> - }
> - if ('images' in data) {
> - // TODO: download and cache the images locally
> - // (grab them from the webview’s cache, if possible),
> - // and forward local URLs.
> - mimedata.urls = data.images
> - }
> - return mimedata
> - }
> + property QtObject contextModel: null
> + property real devicePixelRatio: 1.0
>
> function computeBounds(data) {
> - return Qt.rect(data.left * data.scaleX, data.top * data.scaleY,
> - data.width * data.scaleX, data.height * data.scaleY)
> + var locationBarOffset = _webview.locationBarController.height + _webview.locationBarController.offset
> + var scaleX = data.outerWidth / data.innerWidth * internal.devicePixelRatio
> + var scaleY = data.outerHeight / (data.innerHeight + locationBarOffset) * internal.devicePixelRatio
> + return Qt.rect(data.left * scaleX, data.top * scaleY + locationBarOffset,
> + data.width * scaleX, data.height * scaleY)
> }
>
> function dismissCurrentContextualMenu() {
> - if (currentContextualMenu != null) {
> - PopupUtils.close(currentContextualMenu)
> + if (contextModel) {
> + contextModel.close()
> }
> }
>
> - function dismissCurrentSelection() {
> - if (currentSelection != null) {
> - // For some reason a 0 delay fails to destroy the selection
> - // when it was requested upon a screen orientation change…
> - currentSelection.destroy(1)
> - }
> - }
> + // Automatically clear the contextual data when the context model is destroyed
> + readonly property var contextualDataCleaner: contextModel ? 0 : _webview.contextualData.clear()
Yeah, this is pretty much equivalent. I’ll change to your suggestion, it’s more readable indeed.
> }
>
> readonly property bool lastLoadSucceeded: internal.lastLoadRequestStatus === Oxide.LoadEvent.TypeSucceeded
>
> === modified file 'src/app/FileExtensionMapper.js'
> --- src/app/FileExtensionMapper.js 2014-11-11 15:07:46 +0000
> +++ src/app/FileExtensionMapper.js 2015-08-25 15:14:26 +0000
> @@ -217,3 +221,1009 @@
> return ContentType.Unknown;
> }
> }
> +
> +// Constructed from /etc/mime.types
Even though that code was generated with a quick python script (I didn’t go and write this all manually), you’re right, this is not scalable, and is annoying to maintain.
I didn’t know of QMimeDatabase, it looks like the perfect tool for this use case.
My only concern was that webapps run confined (and in the future the browser will too), and QMimeDatabase needs to read ~/.local/share/mime/mime.cache and potentially files under /usr/share/mime/ if the cache file has been deleted, but I just checked and the ubuntu-webapp apparmor profile allows read access to those files, so we should be covered.
I’ll see about exposing QMimeDatabase to QML and using that instead of that huge switch() function.
> +function filenameToMimeType(filename) {
> + switch(getExtension(filename)) {
> + case "%":
> + case "bak":
> + case "old":
> + case "sik":
> + case "~":
> + return "application/x-trash";
> + case "323":
> + return "text/h323";
> + case "3gp":
> + return "video/3gpp";
> + case "7z":
> + return "application/x-7z-compressed";
> + case "abw":
> + return "application/x-abiword";
> + case "ai":
> + case "eps":
> + case "eps2":
> + case "eps3":
> + case "epsf":
> + case "epsi":
> + case "ps":
> + return "application/postscript";
> + case "aif":
> + case "aifc":
> + case "aiff":
> + return "audio/x-aiff";
> + case "alc":
> + return "chemical/x-alchemy";
> + case "amr":
> + return "audio/amr";
> + case "anx":
> + return "application/annodex";
> + case "apk":
> + return "application/vnd.android.package-archive";
> + case "appcache":
> + return "text/cache-manifest";
> + case "art":
> + return "image/x-jg";
> + case "asc":
> + case "brf":
> + case "pot":
> + case "srt":
> + case "text":
> + case "txt":
> + return "text/plain";
> + case "asf":
> + case "asx":
> + return "video/x-ms-asf";
> + case "asn":
> + return "chemical/x-ncbi-asn1";
> + case "asn":
> + return "chemical/x-ncbi-asn1-spec";
> + case "asn":
> + return "chemical/x-ncbi-asn1";
> + case "asn":
> + return "chemical/x-ncbi-asn1-spec";
> + case "aso":
> + case "val":
> + return "chemical/x-ncbi-asn1-binary";
> + case "atom":
> + return "application/atom+xml";
> + case "atomcat":
> + return "application/atomcat+xml";
> + case "atomsrv":
> + return "application/atomserv+xml";
> + case "au":
> + case "snd":
> + return "audio/basic";
> + case "avi":
> + return "video/x-msvideo";
> + case "awb":
> + return "audio/amr-wb";
> + case "axa":
> + return "audio/annodex";
> + case "axv":
> + return "video/annodex";
> + case "b":
> + return "chemical/x-molconn-Z";
> + case "bat":
> + case "com":
> + case "dll":
> + case "exe":
> + return "application/x-msdos-program";
> + case "bcpio":
> + return "application/x-bcpio";
> + case "bib":
> + return "text/x-bibtex";
> + case "bin":
> + return "application/octet-stream";
> + case "bmp":
> + return "image/x-ms-bmp";
> + case "boo":
> + return "text/x-boo";
> + case "book":
> + case "fb":
> + case "fbdoc":
> + case "fm":
> + case "frame":
> + case "frm":
> + case "maker":
> + return "application/x-maker";
> + case "bsd":
> + return "chemical/x-crossfire";
> + case "c":
> + return "text/x-csrc";
> + case "c++":
> + case "cc":
> + case "cpp":
> + case "cxx":
> + return "text/x-c++src";
> + case "c3d":
> + return "chemical/x-chem3d";
> + case "cab":
> + return "application/x-cab";
> + case "cac":
> + case "cache":
> + return "chemical/x-cache";
> + case "cap":
> + case "pcap":
> + return "application/vnd.tcpdump.pcap";
> + case "cascii":
> + case "cbin":
> + case "ctab":
> + return "chemical/x-cactvs-binary";
> + case "cat":
> + return "application/vnd.ms-pki.seccat";
> + case "cbr":
> + return "application/x-cbr";
> + case "cbz":
> + return "application/x-cbz";
> + case "cda":
> + case "cdf":
> + return "application/x-cdf";
> + case "cdr":
> + return "image/x-coreldraw";
> + case "cdt":
> + return "image/x-coreldrawtemplate";
> + case "cdx":
> + return "chemical/x-cdx";
> + case "cdy":
> + return "application/vnd.cinderella";
> + case "cef":
> + case "cxf":
> + return "chemical/x-cxf";
> + case "cer":
> + return "chemical/x-cerius";
> + case "chm":
> + return "chemical/x-chemdraw";
> + case "chrt":
> + return "application/x-kchart";
> + case "cif":
> + return "chemical/x-cif";
> + case "class":
> + return "application/java-vm";
> + case "cls":
> + case "ltx":
> + case "sty":
> + case "tex":
> + return "text/x-tex";
> + case "cmdf":
> + return "chemical/x-cmdf";
> + case "cml":
> + return "chemical/x-cml";
> + case "cod":
> + return "application/vnd.rim.cod";
> + case "cpa":
> + return "chemical/x-compass";
> + case "cpio":
> + return "application/x-cpio";
> + case "cpt":
> + return "image/x-corelphotopaint";
> + case "cpt":
> + return "application/mac-compactpro";
> + case "cpt":
> + return "image/x-corelphotopaint";
> + case "cpt":
> + return "application/mac-compactpro";
> + case "cr2":
> + return "image/x-canon-cr2";
> + case "crl":
> + return "application/x-pkcs7-crl";
> + case "crt":
> + return "application/x-x509-ca-cert";
> + case "crw":
> + return "image/x-canon-crw";
> + case "csd":
> + case "orc":
> + case "sco":
> + return "audio/csound";
> + case "csf":
> + return "chemical/x-cache-csf";
> + case "csh":
> + return "application/x-csh";
> + case "csh":
> + return "text/x-csh";
> + case "csh":
> + return "application/x-csh";
> + case "csh":
> + return "text/x-csh";
> + case "csm":
> + case "csml":
> + return "chemical/x-csml";
> + case "css":
> + return "text/css";
> + case "csv":
> + return "text/csv";
> + case "ctx":
> + return "chemical/x-ctx";
> + case "cu":
> + return "application/cu-seeme";
> + case "cub":
> + return "chemical/x-gaussian-cube";
> + case "d":
> + return "text/x-dsrc";
> + case "davmount":
> + return "application/davmount+xml";
> + case "dcm":
> + return "application/dicom";
> + case "dcr":
> + case "dir":
> + case "dxr":
> + return "application/x-director";
> + case "ddeb":
> + case "deb":
> + case "udeb":
> + return "application/vnd.debian.binary-package";
> + case "deb":
> + case "udeb":
> + return "application/x-debian-package";
> + case "dif":
> + case "dv":
> + return "video/dv";
> + case "diff":
> + case "patch":
> + return "text/x-diff";
> + case "djv":
> + case "djvu":
> + return "image/vnd.djvu";
> + case "dl":
> + return "video/dl";
> + case "dmg":
> + return "application/x-apple-diskimage";
> + case "dms":
> + return "application/x-dms";
> + case "doc":
> + case "dot":
> + return "application/msword";
> + case "docm":
> + return "application/vnd.ms-word.document.macroEnabled.12";
> + case "docx":
> + return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
> + case "dotm":
> + return "application/vnd.ms-word.template.macroEnabled.12";
> + case "dotx":
> + return "application/vnd.openxmlformats-officedocument.wordprocessingml.template";
> + case "dvi":
> + return "application/x-dvi";
> + case "dx":
> + case "jdx":
> + return "chemical/x-jcamp-dx";
> + case "emb":
> + case "embl":
> + return "chemical/x-embl-dl-nucleotide";
> + case "eml":
> + return "message/rfc822";
> + case "ent":
> + case "prt":
> + return "chemical/x-ncbi-asn1-ascii";
> + case "ent":
> + case "pdb":
> + return "chemical/x-pdb";
> + case "ent":
> + case "prt":
> + return "chemical/x-ncbi-asn1-ascii";
> + case "ent":
> + case "pdb":
> + return "chemical/x-pdb";
> + case "eot":
> + return "application/vnd.ms-fontobject";
> + case "erf":
> + return "image/x-epson-erf";
> + case "es":
> + return "application/ecmascript";
> + case "etx":
> + return "text/x-setext";
> + case "ez":
> + return "application/andrew-inset";
> + case "fch":
> + case "fchk":
> + return "chemical/x-gaussian-checkpoint";
> + case "fig":
> + return "application/x-xfig";
> + case "flac":
> + return "audio/flac";
> + case "fli":
> + return "video/fli";
> + case "flv":
> + return "video/x-flv";
> + case "gal":
> + return "chemical/x-gaussian-log";
> + case "gam":
> + case "gamin":
> + case "inp":
> + return "chemical/x-gamess-input";
> + case "gan":
> + return "application/x-ganttproject";
> + case "gau":
> + case "gjc":
> + case "gjf":
> + return "chemical/x-gaussian-input";
> + case "gcd":
> + return "text/x-pcs-gcd";
> + case "gcf":
> + return "application/x-graphing-calculator";
> + case "gcg":
> + return "chemical/x-gcg8-sequence";
> + case "gen":
> + return "chemical/x-genbank";
> + case "gf":
> + return "application/x-tex-gf";
> + case "gif":
> + return "image/gif";
> + case "gl":
> + return "video/gl";
> + case "gnumeric":
> + return "application/x-gnumeric";
> + case "gpt":
> + return "chemical/x-mopac-graph";
> + case "gsf":
> + case "pfa":
> + case "pfb":
> + return "application/x-font";
> + case "gsm":
> + return "audio/x-gsm";
> + case "gtar":
> + return "application/x-gtar";
> + case "gz":
> + return "application/gzip";
> + case "h":
> + return "text/x-chdr";
> + case "h++":
> + case "hh":
> + case "hpp":
> + case "hxx":
> + return "text/x-c++hdr";
> + case "hdf":
> + return "application/x-hdf";
> + case "hin":
> + return "chemical/x-hin";
> + case "hqx":
> + return "application/mac-binhex40";
> + case "hs":
> + return "text/x-haskell";
> + case "hta":
> + return "application/hta";
> + case "htc":
> + return "text/x-component";
> + case "htm":
> + case "html":
> + case "shtml":
> + return "text/html";
> + case "hwp":
> + return "application/x-hwp";
> + case "ica":
> + return "application/x-ica";
> + case "ice":
> + return "x-conference/x-cooltalk";
> + case "ico":
> + return "image/vnd.microsoft.icon";
> + case "ics":
> + case "icz":
> + return "text/calendar";
> + case "ief":
> + return "image/ief";
> + case "iges":
> + case "igs":
> + return "model/iges";
> + case "iii":
> + return "application/x-iphone";
> + case "info":
> + return "application/x-info";
> + case "ins":
> + case "isp":
> + return "application/x-internet-signup";
> + case "iso":
> + return "application/x-iso9660-image";
> + case "ist":
> + case "istr":
> + return "chemical/x-isostar";
> + case "jad":
> + return "text/vnd.sun.j2me.app-descriptor";
> + case "jam":
> + return "application/x-jam";
> + case "jar":
> + return "application/java-archive";
> + case "java":
> + return "text/x-java";
> + case "jmz":
> + return "application/x-jmol";
> + case "jng":
> + return "image/x-jng";
> + case "jnlp":
> + return "application/x-java-jnlp-file";
> + case "jp2":
> + case "jpg2":
> + return "image/jp2";
> + case "jpe":
> + case "jpeg":
> + case "jpg":
> + return "image/jpeg";
> + case "jpf":
> + case "jpx":
> + return "image/jpx";
> + case "jpm":
> + return "image/jpm";
> + case "js":
> + return "application/javascript";
> + case "json":
> + return "application/json";
> + case "kar":
> + case "mid":
> + case "midi":
> + return "audio/midi";
> + case "key":
> + return "application/pgp-keys";
> + case "kil":
> + return "application/x-killustrator";
> + case "kin":
> + return "chemical/x-kinemage";
> + case "kml":
> + return "application/vnd.google-earth.kml+xml";
> + case "kmz":
> + return "application/vnd.google-earth.kmz";
> + case "kpr":
> + case "kpt":
> + return "application/x-kpresenter";
> + case "ksp":
> + return "application/x-kspread";
> + case "kwd":
> + case "kwt":
> + return "application/x-kword";
> + case "latex":
> + return "application/x-latex";
> + case "lha":
> + return "application/x-lha";
> + case "lhs":
> + return "text/x-literate-haskell";
> + case "lin":
> + return "application/bbolin";
> + case "lsf":
> + case "lsx":
> + return "video/x-la-asf";
> + case "ly":
> + return "text/x-lilypond";
> + case "lyx":
> + return "application/x-lyx";
> + case "lzh":
> + return "application/x-lzh";
> + case "lzx":
> + return "application/x-lzx";
> + case "m3g":
> + return "application/m3g";
> + case "m3u":
> + return "audio/mpegurl";
> + case "m3u8":
> + return "application/x-mpegURL";
> + case "m4a":
> + case "mp2":
> + case "mp3":
> + case "mpega":
> + case "mpga":
> + return "audio/mpeg";
> + case "man":
> + return "application/x-troff-man";
> + case "mbox":
> + return "application/mbox";
> + case "mcif":
> + return "chemical/x-mmcif";
> + case "mcm":
> + return "chemical/x-macmolecule";
> + case "mdb":
> + return "application/msaccess";
> + case "me":
> + return "application/x-troff-me";
> + case "mesh":
> + case "msh":
> + case "silo":
> + return "model/mesh";
> + case "mif":
> + return "application/x-mif";
> + case "mkv":
> + case "mpv":
> + return "video/x-matroska";
> + case "mm":
> + return "application/x-freemind";
> + case "mmd":
> + case "mmod":
> + return "chemical/x-macromodel-input";
> + case "mmf":
> + return "application/vnd.smaf";
> + case "mml":
> + return "text/mathml";
> + case "mng":
> + return "video/x-mng";
> + case "moc":
> + return "text/x-moc";
> + case "mol":
> + return "chemical/x-mdl-molfile";
> + case "mol2":
> + return "chemical/x-mol2";
> + case "moo":
> + return "chemical/x-mopac-out";
> + case "mop":
> + case "mopcrt":
> + case "mpc":
> + case "zmt":
> + return "chemical/x-mopac-input";
> + case "mov":
> + case "qt":
> + return "video/quicktime";
> + case "movie":
> + return "video/x-sgi-movie";
> + case "mp4":
> + return "video/mp4";
> + case "mpe":
> + case "mpeg":
> + case "mpg":
> + return "video/mpeg";
> + case "mph":
> + return "application/x-comsol";
> + case "ms":
> + return "application/x-troff-ms";
> + case "msi":
> + return "application/x-msi";
> + case "mvb":
> + return "chemical/x-mopac-vib";
> + case "mxf":
> + return "application/mxf";
> + case "mxu":
> + return "video/vnd.mpegurl";
> + case "nb":
> + case "nbp":
> + return "application/mathematica";
> + case "nc":
> + return "application/x-netcdf";
> + case "nef":
> + return "image/x-nikon-nef";
> + case "nwc":
> + return "application/x-nwc";
> + case "o":
> + return "application/x-object";
> + case "oda":
> + return "application/oda";
> + case "odb":
> + return "application/vnd.oasis.opendocument.database";
> + case "odc":
> + return "application/vnd.oasis.opendocument.chart";
> + case "odf":
> + return "application/vnd.oasis.opendocument.formula";
> + case "odg":
> + return "application/vnd.oasis.opendocument.graphics";
> + case "odi":
> + return "application/vnd.oasis.opendocument.image";
> + case "odm":
> + return "application/vnd.oasis.opendocument.text-master";
> + case "odp":
> + return "application/vnd.oasis.opendocument.presentation";
> + case "ods":
> + return "application/vnd.oasis.opendocument.spreadsheet";
> + case "odt":
> + return "application/vnd.oasis.opendocument.text";
> + case "oga":
> + case "ogg":
> + case "opus":
> + case "spx":
> + return "audio/ogg";
> + case "ogv":
> + return "video/ogg";
> + case "ogx":
> + return "application/ogg";
> + case "one":
> + case "onepkg":
> + case "onetmp":
> + case "onetoc2":
> + return "application/onenote";
> + case "opf":
> + return "application/oebps-package+xml";
> + case "orf":
> + return "image/x-olympus-orf";
> + case "otf":
> + case "ttf":
> + return "application/font-sfnt";
> + case "otg":
> + return "application/vnd.oasis.opendocument.graphics-template";
> + case "oth":
> + return "application/vnd.oasis.opendocument.text-web";
> + case "otp":
> + return "application/vnd.oasis.opendocument.presentation-template";
> + case "ots":
> + return "application/vnd.oasis.opendocument.spreadsheet-template";
> + case "ott":
> + return "application/vnd.oasis.opendocument.text-template";
> + case "oza":
> + return "application/x-oz-application";
> + case "p":
> + case "pas":
> + return "text/x-pascal";
> + case "p7r":
> + return "application/x-pkcs7-certreqresp";
> + case "pac":
> + return "application/x-ns-proxy-autoconfig";
> + case "pat":
> + return "image/x-coreldrawpattern";
> + case "pbm":
> + return "image/x-portable-bitmap";
> + case "pcf":
> + case "pcf.Z":
> + return "application/x-font-pcf";
> + case "pcx":
> + return "image/pcx";
> + case "pdf":
> + return "application/pdf";
> + case "pfr":
> + return "application/font-tdpfr";
> + case "pgm":
> + return "image/x-portable-graymap";
> + case "pgn":
> + return "application/x-chess-pgn";
> + case "pgp":
> + return "application/pgp-encrypted";
> + case "pk":
> + return "application/x-tex-pk";
> + case "pl":
> + case "pm":
> + return "text/x-perl";
> + case "pls":
> + return "audio/x-scpls";
> + case "png":
> + return "image/png";
> + case "pnm":
> + return "image/x-portable-anymap";
> + case "potm":
> + return "application/vnd.ms-powerpoint.template.macroEnabled.12";
> + case "potx":
> + return "application/vnd.openxmlformats-officedocument.presentationml.template";
> + case "ppam":
> + return "application/vnd.ms-powerpoint.addin.macroEnabled.12";
> + case "ppm":
> + return "image/x-portable-pixmap";
> + case "pps":
> + case "ppt":
> + return "application/vnd.ms-powerpoint";
> + case "ppsm":
> + return "application/vnd.ms-powerpoint.slideshow.macroEnabled.12";
> + case "ppsx":
> + return "application/vnd.openxmlformats-officedocument.presentationml.slideshow";
> + case "pptm":
> + return "application/vnd.ms-powerpoint.presentation.macroEnabled.12";
> + case "pptx":
> + return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
> + case "prf":
> + return "application/pics-rules";
> + case "psd":
> + return "image/x-photoshop";
> + case "py":
> + return "text/x-python";
> + case "pyc":
> + case "pyo":
> + return "application/x-python-code";
> + case "qgs":
> + case "shp":
> + case "shx":
> + return "application/x-qgis";
> + case "qtl":
> + return "application/x-quicktimeplayer";
> + case "ra":
> + case "ram":
> + case "rm":
> + return "audio/x-pn-realaudio";
> + case "ra":
> + return "audio/x-realaudio";
> + case "ra":
> + case "ram":
> + case "rm":
> + return "audio/x-pn-realaudio";
> + case "ra":
> + return "audio/x-realaudio";
> + case "rar":
> + return "application/rar";
> + case "ras":
> + return "image/x-cmu-raster";
> + case "rb":
> + return "application/x-ruby";
> + case "rd":
> + return "chemical/x-mdl-rdfile";
> + case "rdf":
> + return "application/rdf+xml";
> + case "rdp":
> + return "application/x-rdp";
> + case "rgb":
> + return "image/x-rgb";
> + case "roff":
> + case "t":
> + case "tr":
> + return "application/x-troff";
> + case "ros":
> + return "chemical/x-rosdal";
> + case "rpm":
> + return "application/x-redhat-package-manager";
> + case "rss":
> + return "application/x-rss+xml";
> + case "rtf":
> + return "application/rtf";
> + case "rtx":
> + return "text/richtext";
> + case "rxn":
> + return "chemical/x-mdl-rxnfile";
> + case "scala":
> + return "text/x-scala";
> + case "sce":
> + case "sci":
> + return "application/x-scilab";
> + case "scr":
> + return "application/x-silverlight";
> + case "sct":
> + case "wsc":
> + return "text/scriptlet";
> + case "sd":
> + case "sdf":
> + return "chemical/x-mdl-sdfile";
> + case "sd2":
> + return "audio/x-sd2";
> + case "sda":
> + return "application/vnd.stardivision.draw";
> + case "sdc":
> + return "application/vnd.stardivision.calc";
> + case "sdd":
> + return "application/vnd.stardivision.impress";
> + case "sdf":
> + return "application/vnd.stardivision.math";
> + case "sds":
> + return "application/vnd.stardivision.chart";
> + case "sdw":
> + return "application/vnd.stardivision.writer";
> + case "ser":
> + return "application/java-serialized-object";
> + case "sfd":
> + return "application/vnd.font-fontforge-sfd";
> + case "sfv":
> + return "text/x-sfv";
> + case "sgf":
> + return "application/x-go-sgf";
> + case "sgl":
> + return "application/vnd.stardivision.writer-global";
> + case "sh":
> + return "application/x-sh";
> + case "sh":
> + return "text/x-sh";
> + case "sh":
> + return "application/x-sh";
> + case "sh":
> + return "text/x-sh";
> + case "shar":
> + return "application/x-shar";
> + case "sid":
> + return "audio/prs.sid";
> + case "sig":
> + return "application/pgp-signature";
> + case "sis":
> + return "application/vnd.symbian.install";
> + case "sisx":
> + return "x-epoc/x-sisx-app";
> + case "sit":
> + case "sitx":
> + return "application/x-stuffit";
> + case "skd":
> + case "skm":
> + case "skp":
> + case "skt":
> + return "application/x-koan";
> + case "sldm":
> + return "application/vnd.ms-powerpoint.slide.macroEnabled.12";
> + case "sldx":
> + return "application/vnd.openxmlformats-officedocument.presentationml.slide";
> + case "smi":
> + case "smil":
> + return "application/smil+xml";
> + case "spc":
> + return "chemical/x-galactic-spc";
> + case "spl":
> + return "application/futuresplash";
> + case "spl":
> + return "application/x-futuresplash";
> + case "spl":
> + return "application/futuresplash";
> + case "spl":
> + return "application/x-futuresplash";
> + case "sql":
> + return "application/x-sql";
> + case "src":
> + return "application/x-wais-source";
> + case "stc":
> + return "application/vnd.sun.xml.calc.template";
> + case "std":
> + return "application/vnd.sun.xml.draw.template";
> + case "sti":
> + return "application/vnd.sun.xml.impress.template";
> + case "stl":
> + return "application/sla";
> + case "stw":
> + return "application/vnd.sun.xml.writer.template";
> + case "sv4cpio":
> + return "application/x-sv4cpio";
> + case "sv4crc":
> + return "application/x-sv4crc";
> + case "svg":
> + case "svgz":
> + return "image/svg+xml";
> + case "sw":
> + return "chemical/x-swissprot";
> + case "swf":
> + case "swfl":
> + return "application/x-shockwave-flash";
> + case "sxc":
> + return "application/vnd.sun.xml.calc";
> + case "sxd":
> + return "application/vnd.sun.xml.draw";
> + case "sxg":
> + return "application/vnd.sun.xml.writer.global";
> + case "sxi":
> + return "application/vnd.sun.xml.impress";
> + case "sxm":
> + return "application/vnd.sun.xml.math";
> + case "sxw":
> + return "application/vnd.sun.xml.writer";
> + case "tar":
> + return "application/x-tar";
> + case "taz":
> + case "tgz":
> + return "application/x-gtar-compressed";
> + case "tcl":
> + return "application/x-tcl";
> + case "tcl":
> + case "tk":
> + return "text/x-tcl";
> + case "tcl":
> + return "application/x-tcl";
> + case "tcl":
> + case "tk":
> + return "text/x-tcl";
> + case "texi":
> + case "texinfo":
> + return "application/x-texinfo";
> + case "tgf":
> + return "chemical/x-mdl-tgf";
> + case "thmx":
> + return "application/vnd.ms-officetheme";
> + case "tif":
> + case "tiff":
> + return "image/tiff";
> + case "tm":
> + return "text/texmacs";
> + case "torrent":
> + return "application/x-bittorrent";
> + case "ts":
> + return "video/MP2T";
> + case "tsp":
> + return "application/dsptype";
> + case "tsv":
> + return "text/tab-separated-values";
> + case "ttl":
> + return "text/turtle";
> + case "uls":
> + return "text/iuls";
> + case "ustar":
> + return "application/x-ustar";
> + case "vcard":
> + case "vcf":
> + return "text/vcard";
> + case "vcd":
> + return "application/x-cdlink";
> + case "vcs":
> + return "text/x-vcalendar";
> + case "vmd":
> + return "chemical/x-vmd";
> + case "vms":
> + return "chemical/x-vamas-iso14976";
> + case "vrm":
> + case "vrml":
> + case "wrl":
> + return "x-world/x-vrml";
> + case "vrml":
> + case "wrl":
> + return "model/vrml";
> + case "vsd":
> + case "vss":
> + case "vst":
> + case "vsw":
> + return "application/vnd.visio";
> + case "wad":
> + return "application/x-doom";
> + case "wav":
> + return "audio/x-wav";
> + case "wax":
> + return "audio/x-ms-wax";
> + case "wbmp":
> + return "image/vnd.wap.wbmp";
> + case "wbxml":
> + return "application/vnd.wap.wbxml";
> + case "webm":
> + return "video/webm";
> + case "wk":
> + return "application/x-123";
> + case "wm":
> + return "video/x-ms-wm";
> + case "wma":
> + return "audio/x-ms-wma";
> + case "wmd":
> + return "application/x-ms-wmd";
> + case "wml":
> + return "text/vnd.wap.wml";
> + case "wmlc":
> + return "application/vnd.wap.wmlc";
> + case "wmls":
> + return "text/vnd.wap.wmlscript";
> + case "wmlsc":
> + return "application/vnd.wap.wmlscriptc";
> + case "wmv":
> + return "video/x-ms-wmv";
> + case "wmx":
> + return "video/x-ms-wmx";
> + case "wmz":
> + return "application/x-ms-wmz";
> + case "woff":
> + return "application/font-woff";
> + case "wp5":
> + return "application/vnd.wordperfect5.1";
> + case "wpd":
> + return "application/vnd.wordperfect";
> + case "wvx":
> + return "video/x-ms-wvx";
> + case "wz":
> + return "application/x-wingz";
> + case "x3d":
> + return "model/x3d+xml";
> + case "x3db":
> + return "model/x3d+binary";
> + case "x3dv":
> + return "model/x3d+vrml";
> + case "xbm":
> + return "image/x-xbitmap";
> + case "xcf":
> + return "application/x-xcf";
> + case "xcos":
> + return "application/x-scilab-xcos";
> + case "xht":
> + case "xhtml":
> + return "application/xhtml+xml";
> + case "xlam":
> + return "application/vnd.ms-excel.addin.macroEnabled.12";
> + case "xlb":
> + case "xls":
> + case "xlt":
> + return "application/vnd.ms-excel";
> + case "xlsb":
> + return "application/vnd.ms-excel.sheet.binary.macroEnabled.12";
> + case "xlsm":
> + return "application/vnd.ms-excel.sheet.macroEnabled.12";
> + case "xlsx":
> + return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
> + case "xltm":
> + return "application/vnd.ms-excel.template.macroEnabled.12";
> + case "xltx":
> + return "application/vnd.openxmlformats-officedocument.spreadsheetml.template";
> + case "xml":
> + case "xsd":
> + return "application/xml";
> + case "xpi":
> + return "application/x-xpinstall";
> + case "xpm":
> + return "image/x-xpixmap";
> + case "xsl":
> + case "xslt":
> + return "application/xslt+xml";
> + case "xspf":
> + return "application/xspf+xml";
> + case "xtel":
> + return "chemical/x-xtel";
> + case "xul":
> + return "application/vnd.mozilla.xul+xml";
> + case "xwd":
> + return "image/x-xwindowdump";
> + case "xyz":
> + return "chemical/x-xyz";
> + case "xz":
> + return "application/x-xz";
> + case "zip":
> + return "application/zip";
> + default:
> + return "";
> + }
> +}
>
> === added file 'src/app/actions/Undo.qml'
> --- src/app/actions/Undo.qml 1970-01-01 00:00:00 +0000
> +++ src/app/actions/Undo.qml 2015-08-25 15:14:26 +0000
> @@ -0,0 +1,23 @@
> +/*
So that they can be re-used. They are currently shared between the browser app and the webapp container.
> + * Copyright 2015 Canonical Ltd.
> + *
> + * This file is part of webbrowser-app.
> + *
> + * webbrowser-app is free software; you can redistribute it and/or modify
> + * it under the terms of the GNU General Public License as published by
> + * the Free Software Foundation; version 3.
> + *
> + * webbrowser-app is distributed in the hope that it will be useful,
> + * but WITHOUT ANY WARRANTY; without even the implied warranty of
> + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
> + * GNU General Public License for more details.
> + *
> + * You should have received a copy of the GNU General Public License
> + * along with this program. If not, see <http://www.gnu.org/licenses/>.
> + */
> +
> +import Ubuntu.Components 1.3
> +
> +Action {
> + text: i18n.tr("Undo")
> +}
>
> === modified file 'src/app/webbrowser/Browser.qml'
> --- src/app/webbrowser/Browser.qml 2015-08-16 01:37:08 +0000
> +++ src/app/webbrowser/Browser.qml 2015-08-25 15:14:26 +0000
> @@ -926,41 +926,135 @@
> preferences.localStorageEnabled: true
> preferences.appCacheEnabled: true
>
> + property QtObject contextModel: null
> contextualActions: ActionList {
> Actions.OpenLinkInNewTab {
> - enabled: contextualData.href.toString()
> - onTriggered: browser.openUrlInNewTab(contextualData.href, true)
> + objectName: "openLinkInNewTabContextualAction"
> + enabled: contextModel && contextModel.linkUrl.toString()
> + onTriggered: browser.openUrlInNewTab(contextModel.linkUrl, true)
> }
> Actions.OpenLinkInNewBackgroundTab {
> - enabled: contextualData.href.toString() && ((settings.allowOpenInBackgroundTab === "true") ||
> - ((settings.allowOpenInBackgroundTab === "default") && (formFactor === "desktop")))
> - onTriggered: browser.openUrlInNewTab(contextualData.href, false)
> + objectName: "openLinkInNewBackgroundTabContextualAction"
> + enabled: contextModel && contextModel.linkUrl.toString() &&
> + ((settings.allowOpenInBackgroundTab === "true") ||
> + ((settings.allowOpenInBackgroundTab === "default") &&
> + (formFactor === "desktop")))
> + onTriggered: browser.openUrlInNewTab(contextModel.linkUrl, false)
> }
> Actions.BookmarkLink {
> - enabled: contextualData.href.toString() && browser.bookmarksModel
> - onTriggered: bookmarksModel.add(contextualData.href, contextualData.title, "", "")
> + objectName: "bookmarkLinkContextualAction"
> + enabled: contextModel && contextModel.linkUrl.toString() &&
> + browser.bookmarksModel
> + onTriggered: bookmarksModel.add(contextModel.linkUrl, contextModel.linkText, "", "")
> }
> Actions.CopyLink {
> - enabled: contextualData.href.toString()
> - onTriggered: Clipboard.push(["text/plain", contextualData.href.toString()])
> + objectName: "copyLinkContextualAction"
> + enabled: contextModel && contextModel.linkUrl.toString()
> + onTriggered: Clipboard.push(["text/plain", contextModel.linkUrl.toString()])
> + }
> + Actions.SaveLink {
> + objectName: "SaveLinkContextualAction"
> + enabled: contextModel && contextModel.linkUrl.toString()
> + onTriggered: contextModel.saveLink()
> }
> Actions.ShareLink {
> - enabled: (formFactor == "mobile") && contextualData.href.toString()
> - onTriggered: internal.shareLink(contextualData.href.toString(), contextualData.title)
> + objectName: "ShareLinkContextualAction"
> + enabled: (formFactor == "mobile") &&
> + contextModel && contextModel.linkUrl.toString()
> + onTriggered: internal.shareLink(contextModel.linkUrl.toString(), contextModel.linkText)
> }
> Actions.OpenImageInNewTab {
> - enabled: contextualData.img.toString()
> - onTriggered: browser.openUrlInNewTab(contextualData.img, true)
> + objectName: "OpenImageInNewTabContextualAction"
> + enabled: contextModel && contextModel.srcUrl.toString()
> + onTriggered: browser.openUrlInNewTab(contextModel.srcUrl, true)
> }
> Actions.CopyImage {
> - enabled: contextualData.img.toString()
> - onTriggered: Clipboard.push(["text/plain", contextualData.img.toString()])
> + objectName: "CopyImageContextualAction"
> + enabled: contextModel &&
> + (contextModel.mediaType === Oxide.WebView.MediaTypeImage) &&
> + contextModel.srcUrl.toString()
> + onTriggered: Clipboard.push(["text/plain", contextModel.srcUrl.toString()])
> }
> Actions.SaveImage {
> - enabled: contextualData.img.toString() && downloadLoader.status == Loader.Ready
> - onTriggered: downloadLoader.item.downloadPicture(contextualData.img)
> - }
> - }
> + objectName: "SaveImageContextualAction"
> + enabled: contextModel &&
> + ((contextModel.mediaType === Oxide.WebView.MediaTypeImage) ||
> + (contextModel.mediaType === Oxide.WebView.MediaTypeCanvas)) &&
> + contextModel.srcUrl.toString()
> + onTriggered: contextModel.saveMedia()
> + }
> + Actions.Undo {
> + objectName: "UndoContextualAction"
> + enabled: contextModel && contextModel.isEditable &&
> + (contextModel.editFlags & Oxide.WebView.UndoCapability)
> + onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandUndo)
> + }
> + Actions.Redo {
> + objectName: "RedoContextualAction"
> + enabled: contextModel && contextModel.isEditable &&
> + (contextModel.editFlags & Oxide.WebView.RedoCapability)
> + onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandRedo)
> + }
> + Actions.Cut {
> + objectName: "CutContextualAction"
> + enabled: contextModel && contextModel.isEditable &&
> + (contextModel.editFlags & Oxide.WebView.CutCapability)
> + onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandCut)
> + }
> + Actions.Copy {
> + objectName: "CopyContextualAction"
> + enabled: contextModel && contextModel.isEditable &&
> + (contextModel.editFlags & Oxide.WebView.CopyCapability)
> + onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandCopy)
> + }
> + Actions.Paste {
> + objectName: "PasteContextualAction"
> + enabled: contextModel && contextModel.isEditable &&
> + (contextModel.editFlags & Oxide.WebView.PasteCapability)
> + onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandPaste)
> + }
> + Actions.Erase {
> + objectName: "EraseContextualAction"
> + enabled: contextModel && contextModel.isEditable &&
> + (contextModel.editFlags & Oxide.WebView.EraseCapability)
> + onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandErase)
> + }
> + Actions.SelectAll {
> + objectName: "SelectAllContextualAction"
> + enabled: contextModel && contextModel.isEditable &&
> + (contextModel.editFlags & Oxide.WebView.SelectAllCapability)
> + onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandSelectAll)
> + }
> + }
> +
> + function hasContextActions() {
Good catch. Removed.
> + if (contextualActions) {
> + for (var i in contextualActions.actions) {
> + if (contextualActions.actions[i].enabled) {
> + return true
> + }
> + }
> + }
> + return false
> + }
> +
> + Component {
> + id: contextMenuNarrowComponent
> + ContextMenuMobile {
> + actions: contextualActions
> + Component.onCompleted: webviewimpl.contextModel = contextModel
> + }
> + }
> + Component {
> + id: contextMenuWideComponent
> + ContextMenuWide {
> + webview: webviewimpl
> + parent: browser
> + actions: contextualActions
> + Component.onCompleted: webviewimpl.contextModel = contextModel
> + }
> + }
> + contextMenu: browser.wide ? contextMenuWideComponent : contextMenuNarrowComponent
>
> onNewViewRequested: {
> var tab = tabComponent.createObject(tabContainer, {"request": request, 'incognito': browser.incognito})
>
> === added file 'src/app/webbrowser/ContextMenuWide.qml'
> --- src/app/webbrowser/ContextMenuWide.qml 1970-01-01 00:00:00 +0000
> +++ src/app/webbrowser/ContextMenuWide.qml 2015-08-25 15:14:26 +0000
> @@ -0,0 +1,158 @@
> +/*
> + * Copyright 2015 Canonical Ltd.
> + *
> + * This file is part of webbrowser-app.
> + *
> + * webbrowser-app is free software; you can redistribute it and/or modify
> + * it under the terms of the GNU General Public License as published by
> + * the Free Software Foundation; version 3.
> + *
> + * webbrowser-app is distributed in the hope that it will be useful,
> + * but WITHOUT ANY WARRANTY; without even the implied warranty of
> + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
> + * GNU General Public License for more details.
> + *
> + * You should have received a copy of the GNU General Public License
> + * along with this program. If not, see <http://www.gnu.org/licenses/>.
> + */
> +
> +import QtQuick 2.4
> +import Ubuntu.Components 1.3
> +import Ubuntu.Components.ListItems 1.3 as ListItems
> +import Ubuntu.Components.Popups 1.3 as Popups
> +import com.canonical.Oxide 1.8 as Oxide
> +
> +Popups.Popover {
> + id: contextMenu
> +
> + property QtObject contextModel: model
> + property ActionList actions: null
> + property var webview: null
> +
> + QtObject {
> + id: internal
> +
> + readonly property bool isImage: ((contextModel.mediaType === Oxide.WebView.MediaTypeImage) ||
> + (contextModel.mediaType === Oxide.WebView.MediaTypeCanvas)) &&
> + contextModel.srcUrl.toString()
> +
> + readonly property int lastEnabledActionIndex: {
> + var last = -1
> + for (var i in actions.actions) {
> + if (actions.actions[i].enabled) {
> + last = i
> + }
> + }
> + return last
> + }
> +
> + readonly property real locationBarOffset: contextMenu.webview.locationBarController.height + contextMenu.webview.locationBarController.offset
> + }
> +
> + Rectangle {
> + anchors.fill: parent
> + color: "#ececec"
> + }
> +
> + Column {
> + anchors {
> + left: parent.left
> + right: parent.right
> + }
> +
> + Label {
> + objectName: "titleLabel"
> + text: internal.isImage ? contextModel.srcUrl : contextModel.linkUrl
> + anchors {
> + left: parent.left
> + leftMargin: units.gu(2)
> + right: parent.right
> + rightMargin: units.gu(2)
> + }
> + height: units.gu(5)
> + visible: !contextModel.isEditable
> + fontSize: "x-small"
> + color: "#888888"
> + elide: Text.ElideRight
> + verticalAlignment: Text.AlignVCenter
> + }
> +
> + ListItems.ThinDivider {
> + anchors {
> + left: parent.left
> + leftMargin: units.gu(2)
> + right: parent.right
> + rightMargin: units.gu(2)
> + }
> + visible: !contextModel.isEditable
> + }
> +
> + Repeater {
> + model: actions.actions
> + delegate: ListItems.Empty {
> + readonly property var action: actions.actions[index]
> + objectName: action.objectName + "_item"
> + visible: action.enabled
> + showDivider: false
> +
> + height: units.gu(5)
> +
> + Label {
> + anchors {
> + left: parent.left
> + leftMargin: units.gu(2)
> + right: parent.right
> + rightMargin: units.gu(2)
> + verticalCenter: parent.verticalCenter
> + }
> + fontSize: "small"
> + text: action.text
> + }
> +
> + ListItems.ThinDivider {
> + visible: index < internal.lastEnabledActionIndex
> + anchors {
> + left: parent.left
> + leftMargin: units.gu(2)
> + right: parent.right
> + rightMargin: units.gu(2)
> + bottom: parent.bottom
> + }
> + }
> +
> + onTriggered: {
> + action.trigger()
> + contextMenu.hide()
> + }
> + }
> + }
> + }
> +
> + Item {
> + id: positioner
> + visible: false
> + parent: contextMenu.webview
This is a known regression in the UITK, it will be fixed there: bug #1483708.
> + // XXX: Because the context model’s position is incorrectly reported in
> + // device-independent pixels (see https://launchpad.net/bugs/1471181),
> + // it needs to be multiplied by the device pixel ratio to get physical pixels.
> + x: contextModel.position.x * contextMenu.webview.devicePixelRatio
> + y: contextModel.position.y * contextMenu.webview.devicePixelRatio + internal.locationBarOffset
> + }
> + caller: positioner
> +
> + Component.onCompleted: {
> + if (contextModel.linkUrl.toString() ||
> + contextModel.srcUrl.toString() ||
> + (contextModel.isEditable && contextModel.editFlags)) {
> + show()
> + } else {
> + contextModel.close()
> + }
> + }
> +
> + onVisibleChanged: {
> + if (!visible) {
> + contextModel.close()
> + }
> + }
> +}
>
> === added file 'tests/autopilot/webbrowser_app/tests/test_contextmenu.py'
> --- tests/autopilot/webbrowser_app/tests/test_contextmenu.py 1970-01-01 00:00:00 +0000
> +++ tests/autopilot/webbrowser_app/tests/test_contextmenu.py 2015-08-25 15:14:26 +0000
> @@ -0,0 +1,177 @@
> +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
> +#
> +# Copyright 2015 Canonical
> +#
> +# This program is free software: you can redistribute it and/or modify it
> +# under the terms of the GNU General Public License version 3, as published
> +# by the Free Software Foundation.
> +#
> +# This program is distributed in the hope that it will be useful,
> +# but WITHOUT ANY WARRANTY; without even the implied warranty of
> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
> +# GNU General Public License for more details.
> +#
> +# You should have received a copy of the GNU General Public License
> +# along with this program. If not, see <http://www.gnu.org/licenses/>.
> +
> +import time
> +from autopilot.platform import model
> +from autopilot.matchers import Eventually
> +import testtools
> +from testtools.matchers import Equals, GreaterThan, StartsWith
> +
> +from webbrowser_app.tests import StartOpenRemotePageTestCaseBase
> +
> +
> +class TestContextMenuBase(StartOpenRemotePageTestCaseBase):
> +
> + data_uri_prefix = "data:image/png;base64,"
> +
> + def setUp(self, path):
> + super(TestContextMenuBase, self).setUp(path)
> + self.menu = self.open_context_menu()
> +
> + def open_context_menu(self):
> + webview = self.main_window.get_current_webview()
> + chrome = self.main_window.chrome
> + x = webview.globalRect.x + webview.globalRect.width // 2
> + y = webview.globalRect.y + \
> + (webview.globalRect.height + chrome.height) // 2
> + self.pointing_device.move(x, y)
> + if model() == 'Desktop':
It’s currently not used anywhere else as far as I can tell, which is why I didn’t encapsulate it in an emulator. I would suggest factoring it out whenever the need arises to invoke a context menu in other tests.
> + self.pointing_device.click(button=3)
> + else:
> + self.pointing_device.press()
> + time.sleep(1.5)
> + self.pointing_device.release()
> + return self.main_window.get_context_menu()
> +
> + def click_action(self, name):
> + self.menu.click_action(name)
> + self.menu.wait_until_destroyed()
> +
> + def verify_link_opened_in_a_new_tab(self):
> + self.assert_number_webviews_eventually(2)
> + webview = self.main_window.get_current_webview()
> + new_url = self.base_url + "/test1"
> + self.assertThat(webview.url, Eventually(Equals(new_url)))
> +
> + def verify_link_bookmarked(self):
> + url = self.base_url + "/test1"
> + self.main_window.go_to_url(url)
> + self.main_window.wait_until_page_loaded(url)
> + self.main_window.chrome.bookmarked.wait_for(True)
> +
> + def verify_image_opened_in_a_new_tab(self):
> + self.assert_number_webviews_eventually(2)
> + webview = self.main_window.get_current_webview()
> + self.assertThat(webview.url,
> + Eventually(StartsWith(self.data_uri_prefix)))
> +
> +
> +class TestContextMenuLink(TestContextMenuBase):
> +
> + def setUp(self):
> + super(TestContextMenuLink, self).setUp(path="/link")
> + self.assertThat(self.menu.get_title_label().text,
> + Equals(self.base_url + "/test1"))
> +
> + def test_dismiss_menu(self):
> + if self.main_window.wide:
> + # Verify that clicking outside the menu dismisses it
> + webview_rect = self.main_window.get_current_webview().globalRect
> + actions = self.menu.get_visible_actions()
> + self.assertThat(actions[0].globalRect.x,
> + GreaterThan(webview_rect.x))
> + outside_x = (webview_rect.x + actions[0].globalRect.x) // 2
> + outside_y = webview_rect.y + webview_rect.height // 2
> + self.pointing_device.move(outside_x, outside_y)
> + self.pointing_device.click()
> + else:
> + # Verify that clicking the cancel action dismisses it
> + self.menu.click_cancel_action()
> + self.menu.wait_until_destroyed()
> +
> + def test_open_link_in_new_tab(self):
> + self.click_action("openLinkInNewTabContextualAction")
> + self.verify_link_opened_in_a_new_tab()
> +
> + def test_bookmark_link(self):
> + self.click_action("bookmarkLinkContextualAction")
> + self.verify_link_bookmarked()
> +
> + def test_copy_link(self):
> + # There is no easy way to test the contents of the clipboard,
> + # but we can at least verify that the context menu was dismissed.
> + self.click_action("copyLinkContextualAction")
> +
> + @testtools.skipIf(model() == "Desktop", "on devices only")
> + def test_share_link(self):
> + self.click_action("ShareLinkContextualAction")
> + self.main_window.wait_select_single("ContentShareDialog")
> +
> +
> +class TestContextMenuImage(TestContextMenuBase):
> +
> + def setUp(self):
> + super(TestContextMenuImage, self).setUp(path="/image")
> + self.assertThat(self.menu.get_title_label().text,
> + StartsWith(self.data_uri_prefix))
> +
> + def test_open_image_in_new_tab(self):
> + self.click_action("OpenImageInNewTabContextualAction")
> + self.verify_image_opened_in_a_new_tab()
> +
> + def test_copy_image(self):
> + # There is no easy way to test the contents of the clipboard,
> + # but we can at least verify that the context menu was dismissed.
> + self.click_action("CopyImageContextualAction")
> +
> +
> +class TestContextMenuImageAndLink(TestContextMenuBase):
> +
> + def setUp(self):
> + super(TestContextMenuImageAndLink, self).setUp(path="/imagelink")
> + self.assertThat(self.menu.get_title_label().text,
> + StartsWith(self.data_uri_prefix))
> +
> + def test_open_link_in_new_tab(self):
> + self.click_action("openLinkInNewTabContextualAction")
> + self.verify_link_opened_in_a_new_tab()
> +
> + def test_bookmark_link(self):
> + self.click_action("bookmarkLinkContextualAction")
> + self.verify_link_bookmarked()
> +
> + def test_copy_link(self):
> + # There is no easy way to test the contents of the clipboard,
Although of limited usefulness, this test serves to verify that the action is correctly displayed in that context, which I think is useful on its own.
> + # but we can at least verify that the context menu was dismissed.
> + self.click_action("copyLinkContextualAction")
> +
> + @testtools.skipIf(model() == "Desktop", "on devices only")
> + def test_share_link(self):
> + self.click_action("ShareLinkContextualAction")
> + self.main_window.wait_select_single("ContentShareDialog")
> +
> + def test_open_image_in_new_tab(self):
> + self.click_action("OpenImageInNewTabContextualAction")
> + self.verify_image_opened_in_a_new_tab()
> +
> + def test_copy_image(self):
> + # There is no easy way to test the contents of the clipboard,
> + # but we can at least verify that the context menu was dismissed.
> + self.click_action("CopyImageContextualAction")
> +
> +
> +class TestContextMenuTextArea(TestContextMenuBase):
> +
> + def setUp(self):
> + super(TestContextMenuTextArea, self).setUp(path="/textarea")
> + self.assertThat(self.menu.get_title_label().visible, Equals(False))
> +
> + def test_actions(self):
> + actions = ["SelectAll", "Cut", "Undo", "Redo",
> + "Paste", "SelectAll", "Copy", "Erase"]
> + for action in actions:
> + self.click_action("{}ContextualAction".format(action))
> + self.menu = self.open_context_menu()
>
> === added file 'tests/unittests/qml/tst_FileExtensionMapper.qml'
> --- tests/unittests/qml/tst_FileExtensionMapper.qml 1970-01-01 00:00:00 +0000
> +++ tests/unittests/qml/tst_FileExtensionMapper.qml 2015-08-25 15:14:26 +0000
> @@ -0,0 +1,69 @@
> +/*
> + * Copyright 2015 Canonical Ltd.
> + *
> + * This file is part of webbrowser-app.
> + *
> + * webbrowser-app is free software; you can redistribute it and/or modify
> + * it under the terms of the GNU General Public License as published by
> + * the Free Software Foundation; version 3.
> + *
> + * webbrowser-app is distributed in the hope that it will be useful,
> + * but WITHOUT ANY WARRANTY; without even the implied warranty of
> + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
> + * GNU General Public License for more details.
> + *
> + * You should have received a copy of the GNU General Public License
> + * along with this program. If not, see <http://www.gnu.org/licenses/>.
> + */
> +
> +import QtTest 1.0
> +import "../../../src/app/FileExtensionMapper.js" as FileExtensionMapper
> +
> +TestCase {
> + name: "FileExtensionMapper"
> +
> + function test_getExtension_data() {
> + return [
> + {in: "", ext: ""},
> + {in: "pdf", ext: ""},
> + {in: ".vimrc", ext: ""},
> + {in: "example.pdf", ext: "pdf"},
> + {in: "http://example.org/path/example.pdf", ext: "pdf"},
> + {in: "EXAMPLE.PDF", ext: "pdf"}
> + ]
> + }
> +
> + function test_getExtension(data) {
> + compare(FileExtensionMapper.getExtension(data.in), data.ext)
> + }
> +
> + // Test filenameToMimeType for a few selected extensions
> + // (there is no point in testing every single extension).
> + function test_filenameToMimeType_data() {
I’d argue back that it does make sense with the way I initially implemented it (because I wrote a script that parses /etc/mime.types, it could have been buggy, and at the very least this test ensures that a few selected mime types we really care about are correctly handled, even if others aren’t due to a parsing issue).
However after switching to using QMimeDatabase, it won’t make any sense indeed, so I’ll remove it.
> + return [
> + {filename: "document.pdf", mimetype: "application/pdf"},
> + {filename: "document.ps", mimetype: "application/postscript"},
> + {filename: "image.jpg", mimetype: "image/jpeg"},
> + {filename: "image.jpeg", mimetype: "image/jpeg"},
> + {filename: "image.png", mimetype: "image/png"},
> + {filename: "audio.mp3", mimetype: "audio/mpeg"},
> + {filename: "playlist.m3u", mimetype: "audio/mpegurl"},
> + {filename: "video.avi", mimetype: "video/x-msvideo"},
> + {filename: "video.mp4", mimetype: "video/mp4"},
> + {filename: "video.mpg", mimetype: "video/mpeg"},
> + {filename: "video.mpeg", mimetype: "video/mpeg"},
> + {filename: "contact.vcf", mimetype: "text/vcard"},
> + {filename: "contact.vcard", mimetype: "text/vcard"},
> + {filename: "text.txt", mimetype: "text/plain"},
> + {filename: "subtitles.srt", mimetype: "text/plain"},
> + {filename: "compressed.zip", mimetype: "application/zip"},
> + {filename: "compressed.tar.gz", mimetype: "application/gzip"},
> + {filename: "compressed.tgz", mimetype: "application/x-gtar-compressed"},
> + {filename: "shared.torrent", mimetype: "application/x-bittorrent"}
> + ]
> + }
> +
> + function test_filenameToMimeType(data) {
> + compare(FileExtensionMapper.filenameToMimeType(data.filename), data.mimetype)
> + }
> +}
--
https://code.launchpad.net/~osomon/webbrowser-app/oxide-context-menu/+merge/268786
Your team Ubuntu Phablet Team is subscribed to branch lp:webbrowser-app.
More information about the Ubuntu-reviews
mailing list