Index: trac/ticket/query.py
===================================================================
--- trac/ticket/query.py	(revision 7182)
+++ trac/ticket/query.py	(working copy)
@@ -787,6 +787,20 @@
                       req.args.get('page'), 
                       req.args.get('max'))
 
+        if cols:
+            for idx, col in enumerate(cols):
+                if 'up_' + col in req.args:
+                    query.cols[idx] = cols[idx-1]
+                    query.cols[idx-1] = col
+                    req.redirect(query.get_href(req.href))
+                elif 'down_' + col in req.args:
+                    query.cols[idx] = cols[idx+1]
+                    query.cols[idx+1] = col
+                    req.redirect(query.get_href(req.href))
+                elif 'rm_column_' + col in req.args:
+                    query.cols.remove(col)
+                    req.redirect(query.get_href(req.href))
+
         if 'update' in req.args:
             # Reset session vars
             for var in ('query_constraints', 'query_time', 'query_tickets'):
@@ -883,6 +897,14 @@
                 constraint.setdefault('values', []).append('')
                 # FIXME: '' not always correct (e.g. checkboxes)
 
+        # For clients without JavaScript, we add a new column here if
+        # requested
+        cols = data['col']
+        if 'add_column' in req.args:
+            column = req.args.get('new_column')
+            if column:
+                cols.append(column)
+
         req.session['query_href'] = query.get_href(context.href)
         req.session['query_time'] = to_timestamp(orig_time)
         req.session['query_tickets'] = ' '.join([str(t['id'])
@@ -912,9 +934,11 @@
         data.setdefault('description', None)
         data['title'] = title
 
-        data['all_columns'] = query.get_all_columns()
+        all_columns = query.get_all_columns()
+        all_columns.sort(key=lambda x: data['labels'].get(x, x))
         # Don't allow the user to remove the id column        
-        data['all_columns'].remove('id')
+        all_columns.remove('id')
+        data['all_columns'] = all_columns
         data['all_textareas'] = query.get_all_textareas()
 
         add_stylesheet(req, 'common/css/report.css')
Index: trac/ticket/templates/query.html
===================================================================
--- trac/ticket/templates/query.html	(revision 7182)
+++ trac/ticket/templates/query.html	(working copy)
@@ -132,14 +132,37 @@
         <fieldset id="columns">
           <legend>Columns</legend>
           <div>
-            <py:for each="column in all_columns">
-              <label>
-                <input type="checkbox" name="col" value="$column"
-                       checked="${any([(value == column) for value in col])
-                                  and 'checked' or None}" />
+             <table>
+               <tbody>
+                 <tr py:for="idx, column in enumerate(col[1:])">
+                   <td>${labels.get(column, column or 'none')}</td>
+                   <td class="up_down">
+                     <input type="hidden" name="col" value="${column}" />
+                     <input type="submit" name="up_${column}" value="&#8593;"
+                            disabled="${not idx or None}" />&nbsp;
+                     <input type="submit" name="down_${column}" value="&#8595;"
+                            disabled="${idx == len(col)-2 or None}" />
+                   </td>
+                   <td class="actions"><input type="submit" name="rm_column_${column}" value="-" /></td>
+                 </tr>
+               </tbody>
+               <tbody>
+                 <tr class="actions">
+                   <td class="actions" colspan="3" style="text-align: right">
+                     <label for="new_column">Add column</label>&nbsp;
+                     <select name="new_column" id="new_column">
+                       <option></option>
+                       <option py:for="column in all_columns"
+                               py:if="column not in col"
+                               value="${column}">
                 ${labels.get(column, column or 'none')}
-              </label>
-            </py:for>
+                       </option>
+                     </select>
+                     <input type="submit" name="add_column" value="+" />
+                   </td>
+                 </tr>
+               </tbody>
+             </table>
           </div>
         </fieldset>
 
Index: trac/htdocs/js/query.js
===================================================================
--- trac/htdocs/js/query.js	(revision 7182)
+++ trac/htdocs/js/query.js	(working copy)
@@ -273,6 +273,104 @@
       }
       select.selectedIndex = 0;
     }
+
+    // Functionality for adding/removing/redordering columns.  This is a little
+    // disjointed as it's written using jQuery, unlike the above code for dealing
+    // with filters, which was written prior to the use of jQuery in Trac.
+  
+    // Inserts row after insertionPoint (which is another row)
+    function insertColumnRow(row, insertionPoint) {
+      if (!insertionPoint.next().length) {
+        row.find("input[@name^='down_']").attr("disabled", true);
+      }
+      row.find("input[@name^='up_']").attr("disabled", false);
+      
+      if (!insertionPoint.length){
+        $("#columns table").append(row);
+      }
+      else{
+        row.insertAfter(insertionPoint);
+      }
+      insertionPoint.find("input[@name^='down_']").attr("disabled", false);
+      if (!insertionPoint.prev().length) {
+        insertionPoint.find("input[@name^='up_']").attr("disabled", true);
+      }
+    }
+  
+    function removeColumnRow() {
+      var row = $(this).parents("tr");
+  
+      //Disable the Up or Down buttons.
+      if (!row.next().length) {
+         row.prev().find("input[@name^='down_']").attr("disabled", true);
+      }
+      if (!row.prev().length) {
+         row.next().find("input[@name^='up_']").attr("disabled", true);
+      }
+      var column_val = row.find("input[@name^='col']").val();
+      var column_label = row.children().eq(0).text();
+      var select = $("#new_column");
+      var option = $("<option value='" + column_val + "'>" + column_label +
+                     "</option>");
+      //Add item to the appropriate place in the drop-down list.
+      var found = false;
+      select.children().each(function() {
+        if ($.trim($(this).text().toLowerCase()) > $.trim(column_label.toLowerCase())) {
+          $(this).before(option);
+          found = true;
+          return false;
   }
+      });
+      if (!found) select.append(option);
+      select[0].selectedIndex = 0;
 
+      //Remove selected row.
+      row.remove();
+  
+      return false;
+    }
+  
+    function moveColumnUp() {
+      var row = $(this).parents("tr");
+      insertColumnRow(row.prev(), row);
+      return false;
+    }
+  
+    function moveColumnDown() {
+      var row = $(this).parents('tr');
+      insertColumnRow(row, row.next());
+      return false;
+    }
+  
+    $("input[@name^='up_']").click(moveColumnUp);
+    $("input[@name^='down_']").click(moveColumnDown);
+    $("input[@name^='rm_column_']").click(removeColumnRow);
+  
+    // Make the drop-down menu for adding a column a client-side trigger
+    $("input[@name='add_column']").remove();
+    $("#new_column").change(function() {
+      if (this.selectedIndex < 1) return;
+      var option = $(this).children().eq(this.selectedIndex);
+      $(this).children().eq(this.selectedIndex).remove();
+  
+      this.selectedIndex = 0;
+  
+      // The newlines in here are because of FireFox laying things out slightly
+      // differently depending on whitespace -_-
+      var row = $("<tr><td>" + option.text() +"</td><td class='up_down'>" +
+                  "<input type='hidden' name='col' " +
+                         "value='" + option.val() +"' />" +
+                  "<input type='submit' name='up_" + option.val() + "' " +
+                         "value=&#8593; />&nbsp;\n" +
+                  "<input type='submit' name='down_" + option.val() + "' " +
+                         "value=&#8595; /></td><td class='actions'>\n" +
+                  "<input type='submit' name='rm_column_" + option.val() + "' " +
+                         "value='-' />\n</td></tr>");
+      row.find("input[@name^='up_']").click(moveColumnUp);
+      row.find("input[@name^='down_']").click(moveColumnDown);
+      row.find("input[@name^='rm_column_']").click(removeColumnRow);
+      var rows = $("#columns table > tbody").eq(0).children();
+      insertColumnRow(row, rows.eq(rows.length - 1));
+    });
+  }
 })(jQuery);
\ No newline at end of file
Index: trac/htdocs/css/report.css
===================================================================
--- trac/htdocs/css/report.css	(revision 7182)
+++ trac/htdocs/css/report.css	(working copy)
@@ -41,11 +41,17 @@
 #filters td.filter label { padding-right: 1em }
 #filters td.actions { text-align: right; white-space: nowrap }
 
-#columns div label { 
- display: block;
- float: left;
- padding: 0pt 1em .5em 0pt;
+#columns table { width: 100% }
+#columns tr { height: 2em }
+#columns th, #columns td {
+ padding: 0 .2em;
+ vertical-align: middle;
+ white-space: nowrap;
 }
+#columns th { font-size: 11px; text-align: right }
+#columns td label { font-size: 11px }
+#columns td.up_down { width: 100% }
+#columns td.actions { text-align: right }
 
 /* Styles for the report list and the report results table
    (extends the styles for "table.listing") */
