Index: trac/ticket/query.py
===================================================================
--- trac/ticket/query.py	(revision 6737)
+++ trac/ticket/query.py	(working copy)
@@ -41,7 +41,7 @@
                             INavigationContributor, Chrome
 from trac.wiki.api import IWikiSyntaxProvider, parse_args
 from trac.wiki.macros import WikiMacroBase # TODO: should be moved in .api
-from trac.config import Option 
+from trac.config import Option
 
 class QuerySyntaxError(Exception):
     """Exception raised when a ticket query cannot be parsed from a string."""
@@ -94,7 +94,7 @@
         for filter_ in filters:
             filter_ = filter_.split('=')
             if len(filter_) != 2:
-                raise QuerySyntaxError('Query filter requires field and ' 
+                raise QuerySyntaxError('Query filter requires field and '
                                        'constraints separated by a "="')
             field,values = filter_
             if not field:
@@ -253,14 +253,16 @@
         # the default columns, in the same order.  That keeps the query url
         # shorter in the common case where we just want the default columns.
         if cols == self.get_default_columns():
-            cols = None
+            cols = {}
+        constraints = self.constraints.copy()
+        constraints.update(dict(('col' + str(idx), col)
+                                for idx, col in enumerate(cols)))
         return href.query(report=id,
                           order=order, desc=desc and 1 or None,
                           group=self.group or None,
                           groupdesc=self.groupdesc and 1 or None,
-                          col=cols,
                           row=self.rows,
-                          format=format, **self.constraints)
+                          format=format, **constraints)
 
     def to_string(self):
         """Return a user readable and editable representation of the query.
@@ -620,9 +622,7 @@
                     if val.endswith('$USER'): 
                         del constraints[field] 
 
-        cols = req.args.get('col')
-        if isinstance(cols, basestring):
-            cols = [cols]
+        cols = self._get_ordered_cols(req)
         # Since we don't show 'id' as an option to the user,
         # we need to re-insert it here.            
         if cols and 'id' not in cols: 
@@ -637,6 +637,16 @@
                       rows,
                       req.args.get('limit'))
 
+        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))
+
         if 'update' in req.args:
             # Reset session vars
             for var in ('query_constraints', 'query_time', 'query_tickets'):
@@ -659,6 +669,11 @@
         return self.display_html(req, query)
 
     # Internal methods
+    def _get_ordered_cols(self, req):
+        return [item[1] for item in
+                sorted([item for item in req.args.iteritems()
+                        if re.match(r'^col\d+', item[0])],
+                       key=lambda x: (len(x[0]) < 4, int(x[0][3:])))]
 
     def _get_constraints(self, req):
         constraints = {}
@@ -779,7 +794,12 @@
         data.setdefault('description', None)
         data['title'] = title
 
-        data['all_columns'] = query.get_all_columns()
+        all_columns = query.get_all_columns()
+        ordered_cols = self._get_ordered_cols(req)
+        all_columns.sort(key=lambda x: (not x in ordered_cols,
+                                        x in ordered_cols and 
+                                        ordered_cols.index(x)))
+        data['all_columns'] = all_columns
         # Don't allow the user to remove the id column        
         data['all_columns'].remove('id')
         data['all_textareas'] = query.get_all_textareas()
Index: trac/ticket/templates/query.html
===================================================================
--- trac/ticket/templates/query.html	(revision 6737)
+++ trac/ticket/templates/query.html	(working copy)
@@ -136,14 +136,24 @@
         <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}" />
-                ${labels.get(column, column or 'none')}
-              </label>
-            </py:for>
+            <table>
+              <tbody>
+                <tr py:for="idx, column in enumerate(all_columns)">
+                  <td>
+                    <input type="checkbox" name="col${idx+1}" value="$column"
+                           checked="${any([(value == column) for value in col])
+                                      and 'selected' or None}" />
+                  </td>
+                  <td>${labels.get(column, column or 'none')}</td>
+                  <td>
+                    <input type="submit" name="up_${column}" value="Up"
+                           disabled="${not idx or None}" />&nbsp;
+                    <input type="submit" name="down_${column}" value="Down"
+                           disabled="${idx == len(all_columns)-1 or None}" />
+                  </td>
+                </tr>
+              </tbody>
+            </table>
           </div>
         </fieldset>
 
Index: trac/htdocs/js/query.js
===================================================================
--- trac/htdocs/js/query.js	(revision 6737)
+++ trac/htdocs/js/query.js	(working copy)
@@ -273,6 +273,48 @@
       }
       select.selectedIndex = 0;
     }
+
+    $("input[@name^='up_']").click(function() {
+      var row = $(this).parents('tr');
+      if (row.prev().length) {
+        var prev = row.prev();
+        if (!row.next().length) {
+          prev.find("input[@name^='down_']").attr("disabled", true);
+        }
+        prev.find("input[@name^='up_']").attr("disabled", false);
+        var row_check = row.find("input[@type='checkbox']");
+        var prev_check = prev.find("input[@type='checkbox']");
+        var temp = row_check.attr("name");
+        row_check.attr("name", prev_check.attr("name"));
+        prev_check.attr("name", temp);
+        row.insertBefore(row.prev());
+        if (!row.prev().length) {
+          $(this).attr("disabled", true);
+        }
+        row.find("input[@name^='down_']").attr("disabled", false);
+      }
+      return false;
+    });
+    $("input[@name^='down_']").click(function() {
+      var row = $(this).parents('tr');
+      if (row.next().length) {
+        var next = row.next();
+        if (!row.prev().length) {
+          next.find("input[@name^='up_']").attr("disabled", true);
+        }
+        next.find("input[@name^='down_']").attr("disabled", false);
+        var row_check = row.find("input[@type='checkbox']");
+        var next_check = next.find("input[@type='checkbox']");
+        var temp = row_check.attr("name");
+        row_check.attr("name", next_check.attr("name"));
+        next_check.attr("name", temp);
+        row.insertAfter(row.next());
+        if (!row.next().length) {
+          $(this).attr("disabled", true);
+        }
+        row.find("input[@name^='up_']").attr("disabled", false);
+      }
+      return false;
+    });
   }
-
-})(jQuery);
\ No newline at end of file
+})(jQuery);
Index: trac/htdocs/css/report.css
===================================================================
--- trac/htdocs/css/report.css	(revision 6737)
+++ trac/htdocs/css/report.css	(working copy)
@@ -41,6 +41,13 @@
 #filters td.filter label { padding-right: 1em }
 #filters td.actions { text-align: right; white-space: nowrap }
 
+#columns div {
+ height: 15em;
+ overflow: -moz-scrollbars-vertical;
+ overflow-x: hidden;
+ overflow-y: scroll;
+}
+
 #columns div label { 
  display: block;
  float: left;
